Description
We have previously discussed why Gren should support ad-hoc polymorphism (#37). This issue explores parameterized modules as a potential mechanism to deliver that feature.
Modules as a first class construct
The basic idea is to allow modules to depend on a reference to another module that has a specific signature, which can be specified at the import site.
This might best be explained through an example. Keep in mind that the following syntax is not a proposal, just something I made up along the way in order to explain this feature.
Let's say that we're going to implement a Dict
type, and would like it to accept any type, as long as that type is comparable.
We could then define a module signature by creating a Comparable.gren
module with the following content:
signature module Comparable
type alias T
comparable : T -> T -> Order
A module signature is a module that only contains signatures or names, no implementations.
This module can then be used in a Dict
module like:
module Dict requires (Comparable) exposing (..)
type Dict Comparable.T v = ...
get : Comparable.T -> Dict Comparable.T v -> Maybe v
get key dict =
case Comparable.compare key dict.key of
...
With this definition, you can import a Dict
specialised to your specific type, like so:
module MyApp exposing (..)
import MyType
import Dict(MyType where { T = MyType.MyType }) as MyTypeDict
The above would only compile if MyType
has a properly defined compare
function defined.
The where { T = MyType.MyType }
syntax is meant to give people the option "routing" some implementation to a value specified int he signature, in case the names don't match up. There are probably other ways of solving this that doesn't require as much boilerplate at the import site, but the examples above or only meant to get the idea across, not as a an actual proposal.
Benefits
The above scheme enables more flexibility in the Gren system, in that you can write code that works with modules you don't even know about, as long as they fulfil the signature you need.
At the same time, all modules used in such a way will be known at compile time, so it's possible to generate code in a way which avoids dynamic resolution entirely, meaning you don't sacrifice performance to use this feature.
Another benefit with this approach is that it doesn't require the author of a module to explicitly implement a signature. In other words, you can write a signature definition, and have those work with modules you haven't written and have no control over.
Does this support everything we want?
I'm now going to go through each section in #37 and see how this feature answers to each case.
Polymorphic functions and polymorphic data structures
The example above already shows how parametric modules solves the case of polymorphic data structures. The only downside is some added complexity at the import site, and that you'll need to import a parametric module once per unique set of parameters.
For single polymorphic functions, like Array.sort
this might not be ideal.
It would be unreasonable for Array
to require a Comparable
module, when it's only really required for the sorting functions. It also feels a bit much to separate the sorting functions into it's own ArraySort
module, or similar.
Of course, in these cases, you could always change the functions to take in the required functions as arguments instead, like Array.sortWith
.
Magic functions
Parametric modules does little to serve as an implementation for "magic functions".
It would feel overly cumbersome to import operators like ==
using parametric modules every time you'd want to check the structural equality of two values, for example.
Of course, one could argue that such functions should remain magic, as those functions are typically not functions you'd want to implement yourself most of the time anyway. Or at the very least, that a different scheme should be explored for making these particular functions "better".
Mathy operators
Gren's math operators (+
, -
, *
etc.) are currently polymorphic and work for both Int
and Float
.
Parametric modules can work here. We could change the definition of Gren's current math operators to rely on parametric modules, and have a default import of import MathOperators(Int) exposing ((+), (-), (*), ..)
. This default import might then be allowed to be overridden, so that people could "override" these operators to fit their own custom number types.
It would make it impossible to use the same operator for different types within the same module, but that might be a good thing from a readability perspective?
Effect modules
Parametric modules can be used to define the signature of effect modules, which could then be a little bit of logic we could remove from the compiler. This would then be coupled by a built-in API for initialising effect modules with a given signature.
Conclusion
Parametric modules answers some of the use cases listed in #37 quite well, but not all of them.
What I like about parametric modules is that it's made clear where implementations come from, and that you don't have to sacrifice performance to make use of it.
It does require some extra boilerplate at the use site though, but that's a price I'm willing to pay. There are worse things than being explicit...