Implicit function parameters #1272
Replies: 7 comments 33 replies
-
Thanks for this fab proposal. Implicits in this style seem like a much better fit for Gleam than traits, type classes, and so on. I've been looking mostly at the implicits of Scala 3, which seem quite similar to this proposal. // Explicit
[1, 2, 3] |> join(implicit int.to_string) |> io.println() Why is it that the keyword
This could be challenging to parse as we would not know if after seeing The Gleam parser does not back-track so we cannot rewind and try again if we parse something incorrectly. Another syntax idea off the top of my head: implicit get_config(): Config This is less consistent with We could also use type holes (as currently supported in Gleam's type annotations) to make it more concise: implicit Config = get_config() // annotate the type
implicit _ = get_config() // type is purely inferred Should it be permitted to omit the type annotation? I see in Scala 3 it is always required. I would be keen to hear any other syntax ideas people have.
I prefer
A great question! I'm leaning towards values only being implicit within the module they are defined in, and to make a value implicit in another module you must mark them as so when importing them.
Block scope sounds good, with module level implicit values using an
I think this will be OK.
Possibly. We need to consider how it would work with optional arguments and labelled arguments too.
What advantages of this might there be beyond a sort of in-code documentation? Resolving implicits for values of unknown typeAt times it may not be possible to infer at the point of use what instance is required with how Gleam types code top to bottom, left to right. Consider this code. import gleam/int
import gleam/float
implicit int.to_string
implicit float.to_string
fn stringify(x: a, to_string: implicit fn(a) -> String) {
to_string(x)
}
pub fn run(x) {
// the type of x is not annotated, we don't know what it is
// As we don't know what x's type is we don't know which implicit to use
let s = stringify(x)
// x has been used as an int, so it must be an int.
x + 1
} Should this code successfully compile? If so we may need to defer resolving instances until after the function has been type checked. If that is the case consider this code where the implicit is declared after the usage. import gleam/int
fn stringify(x: a, to_string: implicit fn(a) -> String) {
to_string(x)
}
pub fn run(x) {
let s = stringify(x)
implicit int.to_string
x + 1
} Should this compile? I think not. We must be careful to only take into account implicit values that are in scope at the point of use. Resolving implicits when there are generic optionsConsider this code: implicit fn(a) -> String = anything_to_string
implicit fn(Int) -> String = int.to_string
fn stringify(x: a, to_string: implicit fn(a) -> String) {
to_string(x)
}
pub fn main() {
stringify(123)
} Should the What about an instance in which we are picking between Should there be some constraint on how type variables can be used? How would we efficiently implement this type lookup from the scope? Currently we implement type and values scopes using HashMaps so access is |
Beta Was this translation helpful? Give feedback.
-
Thanks for the feedback! I'll answer section by section because this will become a lengthy discussion and I don't have time to write everything at once.
We had a discussion where we concluded that a lambda instance cannot be type-parametric in Gleam (aka higher rank polymorphism), meaning you cannot pass an "open type" function/struct around. Imagine we wrapped In other words, in Gleam we cannot have the following bindings: fn do_something(id: fn(a) -> a) {
let x = id(123)
let y = id("abc")
}
fn do_something2() {
let id = fn(x: a) -> a { x }
let x = id(123)
let y = id("abc")
} If the
This is a tough one! fn pick_element(key: k, dict: implicit Map(k, v)) -> v {
dict |> map.get(key)
}
fn main() {
implicit Map(String, Int) = ...
implicit Map(String, Float) = ...
implicit Map(Int, Float) = ...
let x: Int = pick_element("abc") // ok, all type params known
let y = pick_element("abc") // error, v unknown
let z = pick_element(25) // Theoretically can resolve, but I think this should be a compile error?
}
Maybe we need some restrictions to avoid situations like above... I think a rule such as: a generic type cannot be determined by implicitly passed params. All open types must be determined by other parameters or by expected return type.
I am way too underqualified to answer this question😆 |
Beta Was this translation helpful? Give feedback.
-
This other issue has some prior discussion #1244 |
Beta Was this translation helpful? Give feedback.
-
Some more general thoughts about implicit parameters when comparing them to other solutions in the design space. Implicit parameters have the same power as multi-parameter type classes, with two distinctions:
Both can be a blessing or can be a chore. This brings us back to the question what to what purpose we'd like to implement implicts into Gleam. What problem do we like to solve? Implicits solve two problems: configuration depending on context, and overloading off function names. Configuration problemThe configuration problem is best illustrated by this example, which calculates a result depending on the implicit value of type Modulo { Modulo(k: Int) };
fn add(a: Int, b: Int, impl: Modulo) {
a + b % ?Modulo.k
}
fn mul(a, b, impl) {
a * b % ?Modulo.k
}
fn test(a, b, impl) {
add(mul(a, a), mul(b, b))
}
fn main() {
use Modulo(4)
print(test(2, 3)) // Implicitly pass `Modulo(4)` as argument
use Modulo(5)
print(test(2, 3)) // Implicitly pass `Modulo(5)` as argument
} Implicit parameters are a neat solution to pass on configuration data, without mentioning it over and over again. Haskell/PureScript solve this using a Reader monad, which is not an option for Gleam atm. Is this a problem we'd like to solve? OverloadingUsing implicits, we can implement overloading of function names. We can create functions which are polymorphic only over a subset of types, for example which implement equality, or addition and multiplication: mod eq {
type Eq(a) { Eq(eq: fn(a, a) -> Bool) }
fn ==(x: a, y: a, impl: Eq(a)) {
?Eq(a).eq(x, y)
}
fn /=(x: a, y: a, impl: Eq(a)) {
not(eq(x, y))
}
} Then in another module: mod list {
import eq.{Eq}
fn unique(xs: List(a), impl: Eq(a)) -> List(a) {..}
fn has_prefix(xs: List(a), impl: Eq(a)) -> Bool {..}
fn elem(xs: List(a), y: a, impl: Eq(a)} -> Bool {..}
fn lookup(xs: List(#(a,b)), y: a, impl: Eq(a)) -> b {..}
// ...etc
} Do we want to create functions with implicits like described above? When doing this, implicit parameters would be all over the place. Letting users explicitly import givens would become unwieldy quickly and is maybe not that user friendly, especially for developers. On the other hand, I know Erlang has build-in equality for all types. Is it a goal of Gleam to use that? In that case, implicits would be far less common, and this problem would not arise that often. Global uniquenessNext, global uniqueness. The reason Haskell and Rust have type classes and traits, is the "Hashmap problem" or "Set problem". Take the following (hypothetical) Gleam code (translated from this blog post on global uniqueness in Haskell). mod gleam/set {
type Ord(a) = fn(a, a) -> Ordering;
// Insertion needs an ordering
fn insert(into set: Set(a), this member: a, impl: Ord(a)) -> Set(a) {..}
}
mod test/definition {
type T {
X
Y
};
}
mod test/usage1 {
import gleam/set
import test/definition
fn cmp1(a, b) {
case a, b {
X, X -> EQ
X, Y -> LT
Y, X -> GT
Y, Y -> EQ
}
}
use cmp1
fn ins1(set, member) {
set.insert(set, member) // uses cmp1
}
}
mod test/usage2 {
import gleam/set
import test/definition
fn cmp2(a, b) {
case a, b {
X, X -> EQ
X, Y -> GT // Different!
Y, X -> LT // Different!
Y, Y -> EQ
}
}
use cmp2
fn ins2(set, member) {
set.insert(set, member) // uses cmp2
}
}
mod test/main {
import gleam/set
import test/definition
import test/usage1
import test/usage2
fn main() {
let test_same = set.empty() |> ins1(X) |> ins1(Y) |> ins1(X)
let test_diff = set.empty() |> ins2(X) |> ins1(Y) |> ins1(X)
print(set.to_list(test_same)) // OK: prints [X,Y]
print(set.to_list(test_diff)) // ERROR: prints [X,Y,X]!!!
}
} The module Scala doesn't run into this problem, because every object has its own SummarisingSo, when compared to multi-parameter type classes (or traits), implicits give us the power to manually control the scope of implicits, but we as developers need to be explicit about when they are in scope and when they are imported. Also, they won't give us global uniqueness of implicits/instances/implementations. But the question is: what do we like for Gleam and why? |
Beta Was this translation helpful? Give feedback.
-
Then, another consequence of implicits: the order of top level declarations matter, including imports! Currently, Gleam is a declarative language on the top level: it doesn't matter in which order you define your functions, and in which order you import functions or types from other modules. When introducing implicits, order does matter. For example, moving a function definition can change its meaning, as below example shows with different placements ( fn f() {
println(?Int) // => ERROR: no implicit of type `Int` in scope
}
use 42
fn g() {
println(?Int) // => Will use "42"
}
use 37
fn h() {
println(?Int) // => Will use "37"
}
fn main() {
f()
g() // => prints "42"
h() // => prints "37"
} The same holds for importing givens from different modules, swapping the order of imports changes the meaning of the program. import a.{given Int}
import b.{given Int}
fn f() {
println(?Int) // => Will use `b`'s int
} Maybe it doesn't matter at first sight, but it can be confusing. Also, tooling which auto sorts imports are not an option any more. This boils down to the question: do we intend Gleam to be declarative on the top level or not? Rust, Haskell, Elm, PureScript are, OCaml, F#, Scale aren't. |
Beta Was this translation helpful? Give feedback.
-
Another thing to think about when adding implicits to the language, is rules, or when to abstract over implicits and when to fill them. As always, this is best illustrated with an example. Say we already have some way to do equality with a type type Eq(a) = fn(a, a) -> Bool;
type Pair(a, b) {
Pair(a, b) // Just a pair of `a` and `b`
};
// `equals` takes two pairs of the same type
// We need two implicit arguments to equate both components:
// one of type `a` and one of type `b`
fn equals(x: Pair(a, b), y: Pair(a, b), impl: Eq(a), impl: Eq(b)) -> Bool {
match x, y {
Pair(x1, x2), Pair(y1, y2) ->
eq.eq(x1, y1) && eq.eq(x2, y2)
// needs Eq(a) needs Eq(b)
}
} Now, let us try to bring use equals // ERROR: no givens for `Eq(a)` and `Eq(b) in scope This won't work, as We need to abstract over its implicits when bringing use equals if Eq(a), Eq(b) Which says: "if there is equality for This is comparable to a type class instance in Haskell/PureScript (and also a trait implementation in Rust): instance (Eq a, Eq b) => Eq (Pair a b) where ... So in the end you still need some kind of constraints or rules (as sometimes called in literature) to use implicits. |
Beta Was this translation helpful? Give feedback.
-
Hey everyone, I know this discussion is quite old now, but I would really enjoy a basic version of this idea!' import gleam/int
import gleam/float
import gleam/list
import gleam/string
import gleam/io
pub fn stringify(a: a, f: implicit fn(a) -> String) -> String {
f(a)
}
pub fn join(vals: List(a), sep: String) -> String {
given int.to_string
given float.to_string
given fn(a: String) { a }
list.map(vals, stringify) |> string.join(sep)
}
pub fn main() {
join([1, 2, 3], ", ") |> io.println
join([1.0, 2.0, 3.0], ", ") |> io.println
}
When a function is called without a I've probably repeated/skipped a lot of stuff you have already talked about, but I didn't understand all of it, and I would really like something like this in the language if possible. Feel free to ignore me, roast me, or anything else 😅 but if you could look into this again if only a really simplified version, I would really appreciate it! |
Beta Was this translation helpful? Give feedback.
-
Proposal
The proposed feature allows a function to mark parameters as
implicit
. An implicit parameter is automatically inserted by the compiler if a valid term for its type isgiven
in callsite scope.The
given/implicit
(undecided) keyword should function similar tolet, try, const
. But unlike those, a name is not required:A shorthand to
implicit Type = <expr>
could beimplicit <expr>
whereType
is inferred:A
pub
modifier exposes theimplicit
to other modules.Examples:
Exported implicits can be imported from other modules by type:
Importing by name would look like the following:
Issues to discuss
Naming:
implicit/implicit
,given/implicit
,given/using
?Exporting: Can you expose a
given
term to other modules (withpub
keyword)?Importing: If exportable, do you import a
given
by name or by type? Any syntax to import allgiven
s from a module?Scoping
implicit
parameter be automaticallygiven
in the scope? I think not. You would need to re-give locally withgiven param
.Conflicts/ambiguities
[1, 2, 3] |> join()
vs[1, 2, 3] |> join
?implicit
to let the reader know that it's an explicit injection of animplicit
param?try syntax: Can the following work? I think it should not.
Import givens syntax: Specific:
import lib/config.{given http_config}
, All:import lib/config.{given}
orimport lib/config.{given *}
?Import by type syntax:
import lib/config.{given HttpConfig}
- expects a given constant with this type. We don't care about its name.Should re-exporting be possible?
Notes
Based on Scala 3's given/using mechanism.
I think (explicit) importing by type (instead of name) is powerful. Values can be resolved with the equivalent of Scala's
summon
.Locally
given
expressions should be bound to a compiler-generated variable, whereas globalgiven
consts will be inlined in callsites.Obligatory type class example:
Beta Was this translation helpful? Give feedback.
All reactions