morphir-elm
morphir-elm copied to clipboard
Extensible Record behavior does not match elm, throws run-time type errors
EDIT: I filed this bug based on my understanding of how extensible record types work in Elm; following conversations in https://github.com/finos/morphir/issues/199 , it's clear those are not the expected semantics in Morphir. However, these still cause definite errors in Morphir - in particular, incomprehensible compilation failures and runtime type errors.
Describe the bug Morphir-elm and Ellie seem to accept different things regarding Extensible Records. morphir-elm allows code to compile which throws type errors at run time (in morphir-elm develop, at least.)
To Reproduce
In Ellie, defining an alias to an extensible record requires a type parameter be provided on the alias:
type alias HasA e = {e | name : String }
If you omit the type parameter, Ellie will not compile:
type alias HasA = {e | name : String }
...
The `HasA` type alias uses an unbound type variable `e` in its definition:
8| type alias HasA = {e | name : String }
Morphir-elm accepts both cases (but I believe this causes problems later - see below)
Additionally, there are cases where the type parameter is used where morphir-elm breaks and Ellie works:
type alias HasA e = {e | a : String }
getA : HasA e -> String
getA rec = rec.a
foo : String -> String
foo s =
getA {name = "Bob", a = "A"}
Ellie runs this as expected, morphir-elm fails with:
/usr/local/lib/node_modules/morphir-elm/cli2/Morphir.Elm.CLI.js:726
var _Utils_compare = F2(function(x, y)
^
RangeError: Maximum call stack size exceeded
at Function.f (/usr/local/lib/node_modules/morphir-elm/cli2/Morphir.Elm.CLI.js:726:33)
However, if the type parameter is omitted on the function, morphir-elm works (but Ellie rejects):
type alias HasA e = {e | a : String }
getA : HasA -> String
getA rec = rec.a
foo : String -> String
foo s =
getA {name = "Bob", a = "A"}
This allows for run-time type errors, as from the following code:
type alias HasA e = {e | a : String }
makeList : HasA -> HasA -> List HasA
makeList a b = [a, b]
makeList2 : HasA -> HasA -> List HasA
makeList2 a b = [a, a]
works : String -> List HasA
works s = makeList2 {a = s, number = 5} {a = s, otherNumber = 5}
breaks : String -> List HasA
breaks s = makeList {a = s, number = 5} {a = s, otherNumber = 5}
In the above code, makeList
and makeList2
both take two arguments of type HasA
with no type parameter specified, and return a list (with the type parameter similarly not given. However, makeList
uses them in such a way that their types should agree, while makeList2
allows them to be distinct. This is not apparent on the type level, but is up to the implementation of the function.
This code compiles in morphir-elm (Ellie rejects it due to unspecified type parameters.) In morphir-elm develop, works
gives correct output, but breaks
fails with a type error Output value: Could not unify '{ t9 = a : (), number : t6 }' with '{ t5 = a : (), otherNumber : t2 }' because the fields don't match
(The type error is correct, but I believe it should have been caught at compile time)
Expected behavior I expected the morphir-elm behavior to match the elm behavior and:
- Require type arguments to be specified, both in type aliases and function declarations
- Type errors such as the one shown in the last example to be caught at compile time
Desktop (please complete the following information):
- OS: OSX
- Browser: Safari
- Version: 2.89.0
Additional context I'm working on a Scala-based type checker for Morphir IR, and am trying to nail down the space of what is "Legal" IR, especially with regard to generics/type variables.