Ad-hoc polymorphism in the type system #1244
Replies: 2 comments 20 replies
-
Hello! You've guessed right, there is no firm ideas of what this might look like at present. While these ad-hoc polymorphism features are powerful it is easy to open the door to code that is very difficult to understand and modify, which is something we wish to avoid in Gleam.
Huh! This is the first time I'm hearing about the given/using of Scala 3. That looks very nice in a bunch of ways and I'm quite curious about it now. This looks a lot closer to the kind of system we might have in Gleam one day. I was approaching something like this in my head for an implicits system (tiny not very useful code snippet https://github.com/gleam-experiments/experiments/blob/master/implicit.gleam) but I hadn't got very far. I will have to look into Scala's approach more. |
Beta Was this translation helpful? Give feedback.
-
Warning 1: Very long post! As we're discussing implicits as a possible solution to ad-hoc polymorphism, I'd like to describe some alternative design choices to aid a well founded decision. Implicits solve both ad-hoc polymorphism and configuration contexts, but as this topic is about ad-hoc polymorphism only, I'll only take that into account. Which problems could we solve?
Which solutions do exist?I'll list some solutions ordered from least powerful to most powerful. The structure is as show in below graph. Abbreviations refer to each possible solution presented below. I use trait to describe these solutions, but you can equally well read type class everywhere. All solutions can be implemented in the same way, by passing dictionaries as additional arguments. Overloaded function names (O)Based on Odersky, Wadler, Wehr (1995). One would declare some function names as overloaded. Instances are defined separately. When using such a name in a function body, the compiler infers a type based on the first parameter only. At the call site, the compiler searches for an instance fitting the function constraints. // Declare `+` and `*` as an overloaded name
// Note that we do not give a type signature for how plus should look like
trait +
trait *
// Instantiate for Int and Float
impl +(x, y) { prim_int_add(x, y) }
impl *(x, y) { prim_int_mul(x, y) }
impl +(x, y) { prim_float_add(x, y) }
impl *(x, y) { prim_float_mul(x, y) }
// Use in function
fn addmul(x, y) {
x + x * y + y
}
// The inferred type for `addmul` is
fn(a, a) -> a where + : fn(a, a) -> a, * : fn(a, a) -> a Pros:
Cons:
Single parameter traits (ST)Based on Wadler & Blot (1989) These are the type classes implemented in basic Haskell. // Declare `Calcs` as a trait for numeric calculations
// Note that we do give type signatures here
trait Calcs(a) {
fn +(a, a) -> a
fn *(a, a) -> a
}
// Instantiate for Int and Float
impl Calcs(Int) {
fn +(x, y) { prim_int_add(x, y) }
fn *(x, y) { prim_int_mul(x, y) }
}
impl Calcs(Float) {
fn +(x, y) { prim_float_add(x, y) }
fn *(x, y) { prim_float_mul(x, y) }
}
// Use in function
fn addmul(x, y) {
x + x * y + y
}
// The inferred type for `addmul` is
fn(a, a) -> a where Calcs(a) Pros:
Cons:
Multi-parameter traits (MT)This is a simple extension on single parameter traits: allow more parameters. Implicits as discussed in #1272 are as powerful as this option and share the same benefits and problems. Differences are that, they ask developers to manually bring instances into scope and allow for contextual abstraction. // Declare a trait which collects elements `e` into a collection `c`
trait Collects(e, c) {
fn reduce(over collection: c, with fun: fn(a, e) -> a) -> Result(a, Nil)
fn insert(into collection: c, element x: e) -> c
..
}
// Implement for lists
impl Collects(e, List(e)) {
fn reduce(..) {..}
fn insert(..) { .. }
..
}
// Use in function
fn sum(xs) {
reduce(over: xs, with: (+))
)
// The inferred type for `sum` is
fn(c) -> Result(e, Nil) where Collects(e, c), Calcs(e) However, note that there is no relation between the collection type fn insert_both(x, y, collection) {
collection |> insert(x) |> insert(y)
}
fn bad(collection) {
collection |> insert2(True, 24) // This is OK
}
fn main() {
bad([]) // Error will be here, could be in another module or even different package!
} Because Pros:
Cons:
Parametric traits (PT)Based on Chen, Hudak, Odersky (1992) This is a mixture of single- and multi-parameter traits, but the additional parameters are uniquely determined by the first one. It is similar but not entirely as powerful as Swift protocols and Rust traits, where the single type parameter is the // Declare a trait where collection `c` collects elements `e`
trait c : Collects(e) {
fn reduce(over collection: c, with fun: fn(a, e) -> a) -> Result(a, Nil)
fn insert(into collection: c, element x: e) -> c
..
}
// Implement for lists
impl List(e) : Collects(e) {
fn reduce(..) {..}
fn insert(..) { .. }
..
}
// Use in function
fn sum(xs) {
reduce(over: xs, with: (+))
)
// The inferred type for `sum` is
fn(c) -> Result(e, Nil) where c : Collects(e), e : Calcs Pros:
Cons:
Multi-parameter traits with functional dependencies (MTD)Based on Jones, 2000. This is a more general addition to multi-parameter traits to solve the problems mentioned before. In short, parametrised traits specify a dependency of type variables in one direction. One can generalise this idea to multiple variables in multiple directions. This is what PureScript uses and Haskell with the // Declare a trait where collection `c` collects elements `e` and `e` is uniquely determined by `c`
trait Collects(e, c) | c ~> e {
fn reduce(over collection: c, with fun: fn(a, e) -> a) -> Result(a, Nil)
fn insert(into collection: c, element x: e) -> c
..
}
// Implement for lists
impl Collects(e, List(e)) {
fn reduce(..) {..}
fn insert(..) { .. }
..
}
// Use in function
fn sum(xs) {
reduce(over: xs, with: (+))
)
// The inferred type for `sum` is
fn(c) -> Result(e, Nil) where Collects(c, e), Calcs(e) Pros:
Cons:
Multi-parameter type constructor traits (MCT)Based on Jones (1995). This are traits or type classes on higher-kinded types, which allows us to declare // Declare a trait where collection `c` collects elements `e`
// Note that `c` is used as a _type constructor_ taking `e` as an argument in the signatures below
trait Collects(e, c) {
fn reduce(over collection: c(e), with fun: fn(a, e) -> a) -> Result(a, Nil)
fn insert(into collection: c(e), element x: e) -> c(e)
..
}
// Implement for lists
// Note that `List` is used as a _type constructor_ without arguments
impl Collects(e, List) {
fn reduce(..) {..}
fn insert(..) { .. }
..
}
// Use in function
fn sum(xs) -> {
reduce(over: xs, with: (+))
)
// The inferred type for `sum` is
fn(c(e)) -> Result(e, Nil) where Collects(e, c(e)), Calcs(e) Note that with this extension, one can crate instances for Pros:
Cons:
Multi-parameter type constructor traits with functional dependencies (MCTD)Obviously, we can combine above solution with functional dependencies, which is exactly what Haskell and PureScript do. (Concrete examples left as exercises to the reader ;-)) |
Beta Was this translation helpful? Give feedback.
-
Re #171 (comment)
I kept wondering what that "something" was but could not find any hints, thus I'll start this discussion assuming there's no strong decision made yet, otherwise feel free to close this.
For starters, I like Edward Kmett's "Type Classes vs. the World" not only because it's a good talk but also has some comparisons throughout (specially at the start); of course it's biased towards Haskell's type classes.
Rust's trait system is cool as well.
I have a positive impression of given/using from Scala 3's type classes. I found a video comparing how it has improved from Scala 2 which is quite entertaining. I'm not a Scala developer myself so can't really criticize how it has changed over the years, but it seems that Scala 3's polymorphism has better ergonomics and is saner.
I've also heard about OCaml's upcoming "modular implicits" being an improvement over ML modules but have never really studied what it is myself.
Overall I'm not educated enough to advocate for one or the other so this whole post is just trying to showcase different "typeclass-ish" approaches from other languages which saw some real adoption. Feel free to discuss any other kinds systems which fit functional languages.
Beta Was this translation helpful? Give feedback.
All reactions