Skip to content

Parameterized Modules #81

Open
Open
@robinheghan

Description

@robinheghan

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...

Metadata

Metadata

Assignees

No one assigned

    Labels

    explorationa description of how something could potentially work

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions