Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
323 changes: 322 additions & 1 deletion src/intro/named-parameters.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,332 @@
# Named Parameters

## Type Parameters
The mechanism of named parameters is one of the central features of the Fram
programming language. With the support of other language constructs, named
parameters are used to express a variety of advanced programming features,
such as parametric polymorphism, existential types, record types, and ML-like
module system. Here we give a brief overview of named parameters in Fram.

## Parametric Polymorphism and Type Schemes

Fram supports ML-style parametric polymorphism and type reconstruction.
Briefly, the type of a function is automatically inferred by the compiler
and generalized to be as polymorphic as possible. For instance, consider
the following definition of the identity function.

```fram
let id x = x
```

The compiler infers the type of `id` to be `T -> T` for each type `T`.
To represent such polymorphism over type `T`, the type system assigns
*type schemes* to variables. The type scheme of `id` function is
`{type T} -> T -> T`, where the first arrow `{type T} -> ...` binds
the type parameter `T` in the rest of the type `T -> T`. When a variable
with a type scheme is used, all parameters within curly braces of the
type scheme are instantiated. In case of type parameters, the compiler
guesses the actual types to be used for instantiation based on the
context of the usage. For example, the `id` function can be used with
different types as follows.

```fram
let a = id 42 # instantiates T to Int
let b = id "abc" # instantiates T to String
```

The programmer can also explicitly specify type parameters when defining
the function. For example, the equivalent definition of `id` with an explicit
type parameter is as follows.

```fram
let id {type T} (x : T) = x
```

It is also possible to define functions with multiple type parameters.

```fram
let const {type A, type B} (x : A) (_ : B) = x
```

The type scheme of `const` is `{type A, type B} -> A -> B -> A`.

## Named Type Parameters

Type parameters presented in previous section are *anonymous*, i.e., their
names are not visible outside the definition. Indeed, the programmer has no
means to specify the names of type parameters implicitly introduced by ML-style
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed 'implicitly' to 'explicitly' - type parameters introduced by ML-style inference are implicit, not specified explicitly.

Copilot uses AI. Check for mistakes.
type inference. However, Fram also supports *named type parameters*, which
can be explicitly specified by the programmer. To specify a named type
parameter, the `type` keyword is omitted and only the name of the parameter is
written within curly braces. For example, the definition of `id` function with
a named type parameter is as follows.

```fram
# identity function with a type scheme {T} -> T -> T
let id {T} (x : T) = x
```

When a polymorphic function has a named type parameter, it can be explicitly
instantiated by specifying the name of the type parameter and the actual
type to be used for instantiation. When the explicit instantiation is omitted,
the compiler infers the actual type as usual.

```fram
let intId = id {T=Int}
let strId = id : String -> String # infers T to be String
```

When multiple named type parameters are present, the programmer can specify
the actual types for some of the parameters, and let the compiler infer
the rest. Moreover, the order of the specified parameters can be arbitrary.

```fram
let pair {A, B} (x : A) (y : B) = (x, y)
let p1 = pair {A=Int, B=String} 42 "abc"
let p2 = pair {B=String} 42 "abc" # infers A to be Int
let p3 = pair {B=String, A=Int} 42 "abc"
```

In rare cases, the programmer may want to give a name to a type parameter
that is the same as an existing type in the current scope. In order to avoid
name clashes, the name visible in the scheme might be different from the name
of the type parameter used within the function body. For example, assume that
Comment on lines +90 to +92
Copy link

Copilot AI Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The explanation of name shadowing could be clearer. Consider rephrasing to: 'to avoid name clashes, the name visible in the scheme can be different from the name of the type parameter used within the function body.'

Suggested change
that is the same as an existing type in the current scope. In order to avoid
name clashes, the name visible in the scheme might be different from the name
of the type parameter used within the function body. For example, assume that
that is the same as an existing type in the current scope. To avoid name clashes, the name visible in the scheme can be different from the name of the type parameter used within the function body. For example, assume that

Copilot uses AI. Check for mistakes.
the type `T` is already defined in the current scope. Then, the following
definition abstracts over a type parameter named `T`, but for the purpose
of the definition, the type parameter is referred to as `U`.

```fram
type T = Int # existing type T
let foo {T=U} (x : T -> U) = x 42
```

The same can be done in type schemes. The type scheme of `foo` is
`{T=U} -> (T -> U) -> U`. Note that type variable `U` is bound, so it can
be renamed to e.g., `V` (`{T=V} -> (T -> V) -> V`) without changing the
meaning of the type scheme. Actually, the syntax `{T} -> ...` is just
a syntactic sugar for `{T=T} -> ...`.

## Regular Named Parameters

In Fram, named parameters are not limited to type parameters. Regular
named parameters can also be defined and used in a similar manner.
The names of regular parameters start with a lowercase letter.

```fram
let linear {a : Int, b : Int} x = a * x + b
let intId = linear {a=1, b=0}
let const b = linear {b, a=0} # shorthand for linear {b=b, a=0}
```

As before, the order of specified named parameters can be arbitrary, however,
when instantiating with effectful parameters, the order of evaluation is
always from left to right. In contrast to type parameters, all regular named
parameters must be explicitly specified when using the function.

## Optional Parameters

Fram also supports optional named parameters. Optional parameters have names
starting with a question mark, but bind a variable with the same name without
this character. This variable has type `Option T`, where `T` is the type of
the parameter.

```fram
# The scheme of greet is {?name : String} -> Unit ->> String
let greet {?name} () =
match name with
| Some n => "Hello, " + n + "!"
| None => "Hello, world!"
end
```

Optional parameters can be omitted when the function is used. In this case,
the value `None` is passed to the function. When the parameter is specified,
the value is wrapped in `Some` constructor. Moreover, the programmer can
pass value of type `Option _` directly, when the name used in the
instantiation starts with a question mark.

```fram
let msg1 = greet () # name is None
let msg2 = greet {name="Alice"} () # name is Some "Alice"
let msg3 = greet {?name=None} () # name is None
let msg4 = greet {?name=Some "Bob"} () # name is Some "Bob"
```

## Implicit Parameters

Another useful feature of named parameters in Fram is *implicit parameters*.
Implicit parameters come together with a special namespace for variables,
that have names starting with a tilde (`~`). Name of implicit parameters also
start with a tilde, and if not stated otherwise, they bind variables with the
same names. Implicit parameters can be omitted when the function is used. In
such a case, the compiler resolves the value of the parameter by searching for
a variable with the same name in the current scope.

```fram
let doSomething {~log : String ->> Unit} () =
~log "Doing something important!";
let result = 42 in
~log "Something important is done.";
result
```

To call a function which has implicit parameters, the programmer can either
specify the value of the parameter explicitly, or define a variable with the
same name in the current scope.

```fram
let _ = doSomething {~log=printStrLn} ()
let doWithoutLogging () =
let ~log msg = () in # define a no-op logger
doSomething () # compiler uses the local ~log
```

When a function takes an implicit parameter, it introduces it into the
implicit namespace for the body of the function. Therefore, implicit
parameters can be transitively passed to other functions which also take
implicit parameters.

```fram
let doMore {~log} () =
~log "Starting doing more";
let result = doSomething () in
~log "Finished doing more";
result
```

Same as with other named parameters, the programmer may bind an implicit
parameter to a different name to avoid name clashes. A binder {~name} is just
a syntactic sugar for {~name=~name}.

```fram
let doSomethingElse {~log=logger} () =
logger "Doing something else"
```

## Sections

When programming with named parameters, especially implicit parameters, often
the same named parameters are passed repeatedly to multiple functions. To
avoid such boilerplate code, Fram supports *sections*, which allow grouping
definitions with common named parameters. A named parameter can be declared
at any point within a section using `parameter` keyword, and will be added
to the type schemes of all following definitions that use this parameter.
In particular, declared implicit parameter behaves similarly to dynamically
bound variables in Lisp-like languages, but in a type-safe manner.

```fram
parameter ~log : String ->> Unit
let doSomething () =
~log "Doing something important!";
let result = 42 in
~log "Something important is done.";
result
let doMore () =
~log "Starting doing more";
let result = doSomething () in
~log "Finished doing more";
result
let doMoreTwice () =
doMore ();
doMore ()
let doAllIgnoringLogging () =
let ~log msg = () in
doSomething ();
doMoreTwice ()
```

In the above example, functions `doSomething`, `doMore`, and `doMoreTwice` use
the implicit parameter `~log` directly or indirectly, so their type schemes
include `~log` parameter. On the other hand, `doAllIgnoringLogging` doesn't
have `~log` parameter, because it doesn't use it in its body: it defines a
local `~log` that shadows the implicit parameter. The parameter construct can
also be used to declare other kinds of named parameters. For instance, this
mechanism can be used to name type parameters, but keep the code concise.

```fram
parameter Elem
parameter Acc
let rec foldLeft (f : Acc -> Elem ->> Acc) acc xs =
match xs with
| [] => acc
| y :: ys => foldLeft f (f acc y) ys
end
```

The scope of a parameter declaration extends to the end of the current
section. In most cases it is the end of the current file or module. For local
definitions within a function body, the scope of parameter declaration extends
to the `in` keyword that ends the block of local definitions.

```fram
let foo {~log} () =
parameter ~log : String ->> Unit
# the following two definitions have ~log parameter
let bar () = ~log "In bar"
let baz () = ~log "In baz"; bar ()
in
# the following definition does not have ~log parameter
# The ~log here refers to the parameter of foo
let quux () = ~log "In quux" in
bar ()
```

## Rank-N Types and Higher-Order Functions

At the level of types, named parameters are part of a type scheme rather than
a type. Therefore, named parameters are always instantiated at the time of
usage. This is particularly important for optional parameters, since the
presence or absence of an optional parameter is always clear from the call
site, which avoids possible ambiguities. On the other hand, this design choice
implies that functions with named parameters are not truly first-class values:
they cannot be returned directly from functions. However, Fram allows a form of
Rank-N types for all kinds of named parameters, so it allows functions with
named parameters passed as arguments to other functions. For example, the
following function takes a function with named parameters as an argument and
applies it.

```fram
let foo (f : {X, x : X} -> (X -> _) -> _) =
f {x=42} id
```

The limitation of this mechanism is that arguments with non-trivial type
schemes must be explicitly annotated, as argument `f` in the above example.
This requirement is standard in languages with Rank-N types.

To pass a function with named parameters as an argument, the programmer can
use a lambda expression with named parameters. For example, to call the above
`foo` function with a lambda expression, the following code can be used.

```fram
foo (fn {x} g => g x)
```

The programmer can omit some named parameters in the lambda expression. In
such a case, the omitted parameters will be introduced without giving them
names, so the programmer would not be able to refer to them within the body
of the lambda expression.

However, when the lambda abstraction doesn't bind named parameters at all,
implicit parameters behave differently. In order to mimic the behavior of
dynamically bound variables, implicit parameters are automatically introduced
to the context of the lambda body. For example, consider the following code.

```fram
let logToStdout (f : {~log} -> _) =
f {~log=printStrLn}
let _ = logToStdout (fn () =>
~log "Logging to stdout")
```

In this example, the `~log` parameter is automatically introduced to the body
of the lambda expression, so the programmer can use it directly. This behavior
is specific to implicit parameters; other kinds of named parameters must be
explicitly bound in the lambda abstraction to be used within the body.