Sébastien Besnier
@_sebbes_
What is elm?
Functional language
designed for front end development
The official compiler
elm-in-elm compiler
elm-in-elm compiler
Maintainer: Martin Janiczek @janiczek
What is elm?
Functional language
designed for front end development
What is elm?
Functional language
designed for front end development
What will the talk be about?
- Feedback about onboarding an open source project
- Development tips based on real code
- What is the typical development process in elm?
- How does a compiler work?
Desugar?
Digression
Draw a Christmas tree
Source code
Frontend
Canonical
Typed
Typed
Compiled program
Parse
Desugar
Infer types
Optimized
Emit
Source code
Frontend
Canonical
Typed
Typed
Compiled program
Parse
Desugar
Infer types
Optimized
Emit
Syntactic sugar
Example (Python, JS, ...)
x += 5
x = x + 5
Syntactic sugar
Example (Python, JS, ...)
x += 5
x = x + 5
Desugar
Variable name resolution
Desugar
module Person exposing (..)
import Tree exposing (height)
name = "Sébastien"
c = name
+ " on " + Tree.name
+ " alt: " + height
module Tree exposing
(name, height)
name = "Christmas tree"
height = "4 meters"
module Person exposing (..)
import Tree exposing (height)
name = "Sébastien"
c = name
+ " on " + Tree.name
+ " alt: " + height
module Tree exposing
(name, height)
name = "Christmas tree"
height = "4 meters"
module Person exposing (..)
import Tree exposing (height)
name = "Sébastien"
c = name
+ " on " + Tree.name
+ " alt: " + height
module Tree exposing
(name, height)
name = "Christmas tree"
height = "4 meters"
{ moduleName = Nothing
, varName = "name"}
{ moduleName = Just "Tree"
, varName = "name"}
{ moduleName = Nothing
, varName = "height"}
Frontend
module Person exposing (..)
import Tree exposing (height)
name = "Sébastien"
c = name
+ " on " + Tree.name
+ " alt: " + height
module Tree exposing
(name, height)
name = "Christmas tree"
height = "4 meters"
{ moduleName = Nothing
, varName = "name"}
{ moduleName = Just "Tree"
, varName = "name"}
{ moduleName = Nothing
, varName = "height"}
Frontend
{ moduleName = "Person"
, varName = "name"}
{ moduleName = "Tree"
, varName = "name"}
{ moduleName = "Tree"
, varName = "height"}
Desugar
Canonical
...
, test "desugar variable name in module" <|
\_ ->
Desugar.desugarExpr Dict.empty moduleWithVarA varANotPrefixed
|> mapUnwrap
|> Expect.equal (Ok <| CanonicalU.Var
{ module_ = moduleWithVarA.name, name = "a" })
, test "desugar variable name NOT in this module" <|
\_ ->
Desugar.desugarExpr Dict.empty dummyModule varANotPrefixed
|> Expect.equal
(Err
(CompilerError.VarNameNotFound
{ insideModule = dummyModule.name
, var = { module_ = Nothing, name = "a" }
}
)
)
...
First tests
...
, test "desugar variable name in module" <|
\_ ->
Desugar.desugarExpr Dict.empty moduleWithVarA varANotPrefixed
|> mapUnwrap
|> Expect.equal (Ok <| CanonicalU.Var
{ module_ = moduleWithVarA.name, name = "a" })
, test "desugar variable name NOT in this module" <|
\_ ->
Desugar.desugarExpr Dict.empty dummyModule varANotPrefixed
|> Expect.equal
(Err
(CompilerError.VarNameNotFound
{ insideModule = dummyModule.name
, var = { module_ = Nothing, name = "a" }
}
)
)
...
First tests
First test
Second test
...
, test "desugar variable name in module" <|
\_ ->
Desugar.desugarExpr Dict.empty moduleWithVarA varANotPrefixed
|> mapUnwrap
|> Expect.equal (Ok <| CanonicalU.Var
{ module_ = moduleWithVarA.name, name = "a" })
, test "desugar variable name NOT in this module" <|
\_ ->
Desugar.desugarExpr Dict.empty dummyModule varANotPrefixed
|> Expect.equal
(Err
(CompilerError.VarNameNotFound
{ insideModule = dummyModule.name
, var = { module_ = Nothing, name = "a" }
}
)
)
...
First tests
Each new test asked for its own helper
toTest
{ description = "desugar variable name in module"
, thisModuleName = "A"
, thisModuleVars = [ "a" ]
, thisModuleImports = []
, availableModules = []
, inputVar = ( Nothing, "a" )
, expectedResult = Ok ( "A", "a" )
}
Second try!
toTest
{ description = "desugar variable name in module"
, thisModuleName = "A"
, thisModuleVars = [ "a" ]
, thisModuleImports = []
, availableModules = []
, inputVar = ( Nothing, "a" )
, expectedResult = Ok ( "A", "a" )
}
Second try!
toTest
{ description = "desugar prefixed variable name: import B ; B.a"
, thisModuleName = "A"
, thisModuleVars = [ "a" ]
, thisModuleImports = [ importFromName "B" ]
, availableModules = [ { name = "B", exposedVars = [ "a" ] } ]
, inputVar = ( Just "B", "a" )
, expectedResult = Ok ( "B", "a" )
}
toTest
{ description = "desugar variable name in module"
, thisModuleName = "A"
, thisModuleVars = [ "a" ]
, thisModuleImports = []
, availableModules = []
, inputVar = ( Nothing, "a" )
, expectedResult = Ok ( "A", "a" )
}
Second try!
toTest
{ description = "desugar prefixed variable name: import B ; B.a"
, thisModuleName = "A"
, thisModuleVars = [ "a" ]
, thisModuleImports = [ importFromName "B" ]
, availableModules = [ { name = "B", exposedVars = [ "a" ] } ]
, inputVar = ( Just "B", "a" )
, expectedResult = Ok ( "B", "a" )
}
toTest
{ description = "desugar variable name NOT in this module"
, thisModuleName = "A"
, thisModuleVars = [ "a" ]
, thisModuleImports = []
, availableModules = []
, inputVar = ( Nothing, "b" )
, expectedResult =
Err
(CompilerError.VarNameNotFound
{ insideModule = "A"
, var = { module_ = Nothing, name = "b" }
}
)
}
Use data structures...
Even for tests!
Currying
Desugar
Lambdas in elm
\x y -> x + y
Anonymous function in elm
(x, y) => x+y
JS (ES6) version
function(x, y){ return x+y;}
JS (before ES6) version
Currying
\x y -> x + y
\x -> (\y -> x + y)
is the same as
Currying
\x y -> x + y
\x -> (\y -> x + y)
is the same as
1 function with 2 arguments
2 functions with 1 argument each
Source code
Frontend
Parse
"\x y -> x + y"
String
Source code
Frontend
Parse
"\x y -> x + y"
String
{ arguments =
["x", "y"]
, body = Plus
(Var "x")
(Var "y")
}
\x y -> x + y
Source code
Frontend
Parse
"\x y -> x + y"
String
{ arguments =
["x", "y"]
, body = Plus
(Var "x")
(Var "y")
}
\x y -> x + y
Frontend
Canonical
{ arguments =
["x", "y"]
, body = Plus
(Var "x")
(Var "y")
}
\x y -> x + y
\x -> (\y -> x + y)
Desugar
Frontend
Canonical
{ arguments =
["x", "y"]
, body = Plus
(Var "x")
(Var "y")
}
{ argument = "x"
, body = {
{ argument = "y"
, body = Plus
(Var "x")
(Var "y")
}
}
\x y -> x + y
\x -> (\y -> x + y)
Desugar
Frontend
Canonical
{ arguments =
["x", "y"]
, body = Plus
(Var "x")
(Var "y")
}
{ argument = "x"
, body = {
{ argument = "y"
, body = Plus
(Var "x")
(Var "y")
}
}
\x y -> x + y
\x -> (\y -> x + y)
Desugar
Frontend
Canonical
{ arguments =
["x", "y"]
, body = Plus
(Var "x")
(Var "y")
}
{ argument = "x"
, body = {
{ argument = "y"
, body = Plus
(Var "x")
(Var "y")
}
}
\x y -> x + y
\x -> (\y -> x + y)
Desugar
Frontend
Canonical
{ arguments =
["x", "y"]
, body = Plus
(Var "x")
(Var "y")
}
{ argument = "x"
, body = {
{ argument = "y"
, body = Plus
(Var "x")
(Var "y")
}
}
\x y -> x + y
\x -> (\y -> x + y)
Desugar
- the Frontend ➡️ Canonical step
- how to build expressions (lambdas, "Plus", ...)
- ... in Frontend AND Canonical
Before actually writing test
I need to understand:
Before actually writing test
I need to understand:
- the Frontend ➡️ Canonical step
- how to build expressions (lambdas, "Plus", ...)
- ... in Frontend AND Canonical
- but not the "currying" thing
- Focus on a tiny part of the project
- Easy to complete
- Know the staff and let the staff know you
good first issue
Add records support
user =
{ name = "Seb"
, score = 42
, city = "Paris"
}
Records
user =
{ name = "Seb"
, score = 42
, city = "Paris"
}
Records
Binding
user =
{ name = "Seb"
, score = 42
, city = "Paris"
}
Records
Binding
a =
let b = 42 in
52 + b
user =
{ name = "Seb"
, score = 42
, city = "Paris"
}
Records
Binding
a =
let b = 42 in
52 + b
Binding
Add a functionality in elm
-
add a variant
-
follow the compiler error
Poka-Yoke
Mistakes
Avoid
Let's dive into the code!
Numbering expression
( x + y, zz )
Numbering expression
1
( x + y, zz )
Numbering expression
1
2
( x + y, zz )
Numbering expression
1
2
3
( x + y, zz )
Numbering expression
1
2
3
4
( x + y, zz )
Numbering expression
1
2
3
4
5
( x + y, zz )
Numbering expression
Imperative way (C, Java, Python, ...)
- initialize a counter to 0
- traverse all the AST and increment the counter by 1 at each step
Numbering expression
Imperative way (C, Java, Python, ...)
- initialize a counter to 0
- traverse all the AST and increment the counter by 1 at each step
But all values are immutable in elm!
assignId currentId typedExpr =
((typedExpr, Var currentId), currentId + 1)
assignId currentId typedExpr =
((typedExpr, Var currentId), currentId + 1)
Numbered value
New counter
( x + y, zz )
assignIdsWith currentId expr =
case expr of
Canonical.Var name ->
assignId currentId (Typed.Var name)
...
assignId currentId typedExpr =
((typedExpr, Var currentId), currentId + 1)
2
( x + y, zz )
2
assignIdsWith currentId expr =
case expr of
Canonical.Var name ->
assignId currentId (Typed.Var name)
...
assignId currentId typedExpr =
((typedExpr, Var currentId), currentId + 1)
( x + y, zz )
1
2
3
assignIdsWith currentId expr =
case expr of
Canonical.Var name ->
assignId currentId (Typed.Var name)
Canonical.Plus e1 e2 ->
let
( e1_, id1 ) =
assignIdsWith currentId e1
( e2_, id2 ) =
assignIdsWith id1 e2
in
assignId id2 (Typed.Plus e1_ e2_)
...
( x + y, zz )
1
2
3
assignIdsWith currentId expr =
case expr of
Canonical.Var name ->
assignId currentId (Typed.Var name)
Canonical.Plus e1 e2 ->
let
( e1_, id1 ) =
assignIdsWith currentId e1
( e2_, id2 ) =
assignIdsWith id1 e2
in
assignId id2 (Typed.Plus e1_ e2_)
...
( x + y, zz )
1
2
3
assignIdsWith currentId expr =
case expr of
Canonical.Var name ->
assignId currentId (Typed.Var name)
Canonical.Plus e1 e2 ->
let
( e1_, id1 ) =
assignIdsWith currentId e1
( e2_, id2 ) =
assignIdsWith id1 e2
in
assignId id2 (Typed.Plus e1_ e2_)
...
( x + y, zz )
1
2
3
assignIdsWith currentId expr =
case expr of
Canonical.Var name ->
assignId currentId (Typed.Var name)
Canonical.Plus e1 e2 ->
let
( e1_, id1 ) =
assignIdsWith currentId e1
( e2_, id2 ) =
assignIdsWith id1 e2
in
assignId id2 (Typed.Plus e1_ e2_)
...
( x + y, zz )
4
assignIdsWith currentId expr =
case expr of
Canonical.Var name ->
assignId currentId (Typed.Var name)
Canonical.Plus e1 e2 ->
let
( e1_, id1 ) =
assignIdsWith currentId e1
( e2_, id2 ) =
assignIdsWith id1 e2
in
assignId id2 (Typed.Plus e1_ e2_)
Canonical.Tuple e1 e2 ->
let
( e1_, id1 ) =
assignIdsWith currentId e1
( e2_, id2 ) =
assignIdsWith id1 e2
in
assignId id2 (Typed.Tuple e1_ e2_)
...
5
assignIdsWith currentId expr =
case expr of
...
Canonical.Record bindings ->
let
bindingsList =
Dict.toList bindings
( bindingBodiesList, newId ) =
List.foldl
(\( name, binding ) ( acc, runningId ) ->
let
( body__, nextId ) =
assignIdsWith runningId binding.body
newElt =
( name, { name = name, body = body__ } )
in
( newElt :: acc, nextId )
)
( [], currentId )
bindingsList
in
assignId currentId <|
Typed.Record (Dict.fromList bindingBodiesList)
assignIdsWith currentId expr =
case expr of
...
Canonical.Record bindings ->
let
bindingsList =
Dict.toList bindings
( bindingBodiesList, newId ) =
List.foldl
(\( name, binding ) ( acc, runningId ) ->
let
( body__, nextId ) =
assignIdsWith runningId binding.body
newElt =
( name, { name = name, body = body__ } )
in
( newElt :: acc, nextId )
)
( [], currentId )
bindingsList
in
assignId currentId <|
Typed.Record (Dict.fromList bindingBodiesList)
assignIdsWith currentId expr =
case expr of
...
Canonical.Record bindings ->
let
bindingsList =
Dict.toList bindings
( bindingBodiesList, newId ) =
List.foldl
(\( name, binding ) ( acc, runningId ) ->
let
( body__, nextId ) =
assignIdsWith runningId binding.body
newElt =
( name, { name = name, body = body__ } )
in
( newElt :: acc, nextId )
)
( [], currentId )
bindingsList
in
assignId currentId <|
Typed.Record (Dict.fromList bindingBodiesList)
unused variable newId
assignIdsWith currentId expr =
case expr of
...
Canonical.Record bindings ->
let
bindingsList =
Dict.toList bindings
( bindingBodiesList, newId ) =
List.foldl
(\( name, binding ) ( acc, runningId ) ->
let
( body__, nextId ) =
assignIdsWith runningId binding.body
newElt =
( name, { name = name, body = body__ } )
in
( newElt :: acc, nextId )
)
( [], currentId )
bindingsList
in
assignId currentId <|
Typed.Record (Dict.fromList bindingBodiesList)
unused variable newId
assignIdsWith currentId expr =
case expr of
...
Canonical.Record bindings ->
let
bindingsList =
Dict.toList bindings
( bindingBodiesList, newId ) =
List.foldl
(\( name, binding ) ( acc, runningId ) ->
let
( body__, nextId ) =
assignIdsWith runningId binding.body
newElt =
( name, { name = name, body = body__ } )
in
( newElt :: acc, nextId )
)
( [], currentId )
bindingsList
in
assignId newId <|
Typed.Record (Dict.fromList bindingBodiesList)
elm-analyze
A tool that allows you to analyse your Elm code, identify deficiencies and apply best practices.
elm-analyze
A tool that allows you to analyse your Elm code, identify deficiencies and apply best practices.
Source code
Frontend
Canonical
Typed
Typed
Compiled program
Parse
Desugar
Infer types
Optimized
Emit
Parse records
{ pseudo = "Seb"
, age = 42
}
Record
[ { name = "pseudo"
, body = String "Seb"
}
, { name = "age"
, body = Int 42
}
]
Source code
Frontend
Parse
Parse records
expr =
PP.expression
{ oneOf =
[ if_
, let_
, lambda
, PP.literal literal
, always var
, unit
, list
, tuple
, tuple3
, parenthesizedExpr
]
, ...
}
Parse records
expr =
PP.expression
{ oneOf =
[ if_
, let_
, lambda
, PP.literal literal
, always var
, unit
, list
, tuple
, tuple3
, parenthesizedExpr
, record
]
, ...
}
Parse records
record config =
P.succeed Frontend.Record
|= P.sequence
{ start = P.Token "{" ExpectingRecordLeftBrace
, separator = P.Token "," ExpectingRecordSeparator
, end = P.Token "}" ExpectingRecordRightBrace
, spaces = spacesOnly
, item = binding config
, trailing = P.Forbidden
}
|> P.inContext InRecordRecord
Parse records
record config =
P.succeed Frontend.Record
|= P.sequence
{ start = P.Token "{" ExpectingRecordLeftBrace
, separator = P.Token "," ExpectingRecordSeparator
, end = P.Token "}" ExpectingRecordRightBrace
, spaces = spacesOnly
, item = binding config
, trailing = P.Forbidden
}
|> P.inContext InRecordRecord
On success, will build a Frontend.Record
Parse records
record config =
P.succeed Frontend.Record
|= P.sequence
{ start = P.Token "{" ExpectingRecordLeftBrace
, separator = P.Token "," ExpectingRecordSeparator
, end = P.Token "}" ExpectingRecordRightBrace
, spaces = spacesOnly
, item = binding config
, trailing = P.Forbidden
}
|> P.inContext InRecordRecord
On success, will build a Frontend.Record
Parse records
record config =
P.succeed Frontend.Record
|= P.sequence
{ start = P.Token "{" ExpectingRecordLeftBrace
, separator = P.Token "," ExpectingRecordSeparator
, end = P.Token "}" ExpectingRecordRightBrace
, spaces = spacesOnly
, item = binding config
, trailing = P.Forbidden
}
|> P.inContext InRecordRecord
On success, will build a Frontend.Record
type ParseProblem
= ...
| ExpectingLet
| ExpectingIn
| ExpectingUnit
| ExpectingRecordLeftBrace
| ExpectingRecordSeparator
| ExpectingRecordRightBrace
| InvalidNumber
...
Parse records
record config =
P.succeed Frontend.Record
|= P.sequence
{ start = P.Token "{" ExpectingRecordLeftBrace
, separator = P.Token "," ExpectingRecordSeparator
, end = P.Token "}" ExpectingRecordRightBrace
, spaces = spacesOnly
, item = binding config
, trailing = P.Forbidden
}
|> P.inContext InRecordRecord
On success, will build a Frontend.Record
Parse records
record config =
P.succeed Frontend.Record
|= P.sequence
{ start = P.Token "{" ExpectingRecordLeftBrace
, separator = P.Token "," ExpectingRecordSeparator
, end = P.Token "}" ExpectingRecordRightBrace
, spaces = spacesOnly
, item = binding config
, trailing = P.Forbidden
}
|> P.inContext InRecordRecord
On success, will build a Frontend.Record
binding config =
P.succeed Binding
|= varName
|. P.spaces
|. P.symbol (P.Token "=" ExpectingEqualsSign)
|. P.spaces
|= PP.subExpression 0 config
|> P.inContext InLetBinding
Parse records
record config =
P.succeed Frontend.Record
|= P.sequence
{ start = P.Token "{" ExpectingRecordLeftBrace
, separator = P.Token "," ExpectingRecordSeparator
, end = P.Token "}" ExpectingRecordRightBrace
, spaces = spacesOnly
, item = binding config
, trailing = P.Forbidden
}
|> P.inContext InRecordRecord
On success, will build a Frontend.Record
binding config =
P.succeed Binding
|= varName
|. P.spaces
|. P.symbol (P.Token "=" ExpectingEqualsSign)
|. P.spaces
|= PP.subExpression 0 config
|> P.inContext InLetBinding
Parse and keep!
Parse and keep!
Parse records
record config =
P.succeed Frontend.Record
|= P.sequence
{ start = P.Token "{" ExpectingRecordLeftBrace
, separator = P.Token "," ExpectingRecordSeparator
, end = P.Token "}" ExpectingRecordRightBrace
, spaces = spacesOnly
, item = binding config
, trailing = P.Forbidden
}
|> P.inContext InRecordRecord
On success, will build a Frontend.Record
binding config =
P.succeed Binding
|= varName
|. P.spaces
|. P.symbol (P.Token "=" ExpectingEqualsSign)
|. P.spaces
|= PP.subExpression 0 config
|> P.inContext InLetBinding
Parse and keep!
Parse and keep!
Parse and forget!
Parser only is for compiler stuff
Parser only is for compiler stuff
Regexes are enough for most other use cases
What Regex for email?
(case insensitive mode)
[A-Z0-9-_+].+@[A-Z0-9.-]+\.[A-Z]{2,}
[A-Z0-9-_+.]+@[A-Z0-9.-]+.[A-Z]{2,}
[A-Z0-9-_+.]+@[A-Z0-9.-]+\.[A-Z]{2,}
[A-Z0-9-_+.]+@[A-Z0-9.-+]\.[A-Z]{2,}
What Regex for email?
[A-Z0-9-_+].+@[A-Z0-9.-]+\.[A-Z]{2,}
[A-Z0-9-_+.]+@[A-Z0-9.-]+.[A-Z]{2,}
[A-Z0-9-_+.]+@[A-Z0-9.-]+\.[A-Z]{2,}
[A-Z0-9-_+.]+@[A-Z0-9.-+]\.[A-Z]{2,}
should be after the ]
should be escaped
should be in the [ ]
Markdown?
Markdown?
Possibility to have
nested blocks
Markdown?
Possibility to have
nested blocks
Cannot be recognized with a regex
(consequence of the "Pumping lemma")
Search bar?
Search bar?
Search bar?
Complex (nested) request!
Other use cases for parsers
- Non standard format (ill formed CSV, sprites info, ...)
- Provide DSL to your user
- Bots for instant messaging (Slack, Discord,...)
- ...
module Main exposing (main)
main =
{ x = 42, y = "Seb" }
module Main exposing (main)
main =
{ x = 42, y = "Seb" }
$ make
Compilation finished, writing output to `out.js`.
---------------------------
-- WRITING TO FS ----------
---------------------------
const Main$main = {x : 42, y : "Seb"};
module Main exposing (main)
main =
{ x = 42, y = "Seb" }
$ make
Compilation finished, writing output to `out.js`.
---------------------------
-- WRITING TO FS ----------
---------------------------
const Main$main = {x : 42, y : "Seb"};
Martin:
"I'll want tests"
Fuzzy testing
(property based testing)
Fuzzy testing
(property based testing)
fuzz string "reverse twice is identity" <|
\randomlyGeneratedString ->
randomlyGeneratedString
|> String.reverse
|> String.reverse
|> Expect.equal randomlyGeneratedString
Source code
Frontend
Canonical
Typed
Typed
Compiled program
Parse
Infer types
Optimized
Emit
Desugar
Fuzzy test
- generate an expression
- check that the infered type is correct
Writing tests...
Searching for the bug...
... a long time!
Do you remember that?
type Type
= {- READ THIS!
When adding a case that recurs on Type, you'll have to add a case to
`InferTypes.Unify.unify`:
| MyNewType Type Type
will have to get a case:
(MyNewType m1e1 m1e2, MyNewType m2e1 m2e2) ->
substitutionMap
|> unify m1e1 m2e1
|> Result.andThen (unify m1e2 m2e2)
-}
Var Int
| Function Type Type
| Int
| Float
| Char
| String
...
unify t1 t2 substitutionMap =
case ( t1, t2 ) of
...
( List list1, List list2 ) ->
unify list1 list2 substitutionMap
( Tuple t1e1 t1e2, Tuple t2e1 t2e2 ) ->
substitutionMap
|> unify t1e1 t2e1
|> Result.andThen (unify t1e2 t2e2)
...
_ ->
Err ( TypeMismatch t1 t2, substitutionMap )
unify t1 t2 substitutionMap =
case ( t1, t2 ) of
...
( List list1, List list2 ) ->
unify list1 list2 substitutionMap
( Tuple t1e1 t1e2, Tuple t2e1 t2e2 ) ->
substitutionMap
|> unify t1e1 t2e1
|> Result.andThen (unify t1e2 t2e2)
( Record bindings1, Record bindings2 ) ->
...
...
_ ->
Err ( TypeMismatch t1 t2, substitutionMap )
Poka-Yoke?
Mistakes
Avoid
unify t1 t2 substitutionMap =
case ( t1, t2 ) of
...
( List list1, List list2 ) ->
unify list1 list2 substitutionMap
( Tuple t1e1 t1e2, Tuple t2e1 t2e2 ) ->
substitutionMap
|> unify t1e1 t2e1
|> Result.andThen (unify t1e2 t2e2)
...
_ ->
Err ( TypeMismatch t1 t2, substitutionMap )
Force the dev to handle new type?
Test for all couples (T1, T2)?
Test for all couples (T1, T2)?
18 types => 18² = 324 cases!
And growing very fast!
unify t1 t2 substitutionMap =
case ( t1, t2 ) of
...
( List list1, List list2 ) ->
unify list1 list2 substitutionMap
( List _, _ ) ->
Err (TypeMismatch t1 t2, substitutionMap)
( Tuple t1e1 t1e2, Tuple t2e1 t2e2 ) ->
substitutionMap
|> unify t1e1 t2e1
|> Result.andThen (unify t1e2 t2e2)
( Tuple _, _ ) ->
Err (TypeMismatch t1 t2, substitutionMap)
...
_ ->
Err ( TypeMismatch t1 t2, substitutionMap )
unify t1 t2 substitutionMap =
case ( t1, t2 ) of
...
( List list1, List list2 ) ->
unify list1 list2 substitutionMap
( List _, _ ) ->
Err (TypeMismatch t1 t2, substitutionMap)
( Tuple t1e1 t1e2, Tuple t2e1 t2e2 ) ->
substitutionMap
|> unify t1e1 t2e1
|> Result.andThen (unify t1e2 t2e2)
( Tuple _, _ ) ->
Err (TypeMismatch t1 t2, substitutionMap)
...
_ ->
Err ( TypeMismatch t1 t2, substitutionMap )
36 cases
add 2 cases when adding a type
Numbering? Unify?
Numbering? Unify?
Type inference!
Source code
Frontend
Canonical
Typed
Typed
Compiled program
Parse
Desugar
Infer types
Optimized
Emit
f (x + y) == "hello"
Type of:
f (x + y)
f (x + y)
x and y has to be Ints
f (x + y)
x and y has to be Ints
Int
f (x + y)
x and y has to be Ints
Int
f : Int → ??
f (x + y)
x and y has to be Ints
Int
f : Int → ??
??
f (x + y) == "hello"
x and y has to be Ints
Int
f : Int → ??
??
f (x + y) == "hello"
x and y has to be Ints
Int
f : Int → ??
String
??
f (x + y) == "hello"
x and y has to be Ints
Int
f : Int → ??
String
??
?? has to be String
the result is a Bool
f (x + y) == "hello"
x and y has to be Ints
Int
f : Int → String
String
String
?? has to be String
the result is a Bool
f : Int -> String
x : Int
y : Int
Canonical
Typed
Infer types
Canonical
Typed
Infer types
Numbering
Generate equations
Unify
(solve the equations)
Canonical
Typed
Numbering
Generate equations
Unify
f (x + y) == "hello"
1
2
3
Canonical
Typed
Numbering
Generate equations
Unify
f (x + y) == "hello"
1
2
3
1 == Int 2 == Int 3 == Int
Canonical
Typed
Numbering
Generate equations
Unify
f (x + y) == "hello"
1
2
3
1 == Int 2 == Int 3 == Int
Substitution Map
1 : Int 2 : Int 3 : Int
Canonical
Typed
Numbering
Generate equations
Unify
f (x + y) == "hello"
1
2
3
4
5
1 == Int 2 == Int 3 == Int
Substitution Map
1 : Int 2 : Int 3 : Int
Canonical
Typed
Numbering
Generate equations
Unify
f (x + y) == "hello"
1
2
3
4
5
1 == Int 2 == Int 3 == Int
4 == 3 -> 5
Substitution Map
1 : Int 2 : Int 3 : Int
Canonical
Typed
Numbering
Generate equations
Unify
f (x + y) == "hello"
1
2
3
4
5
1 == Int 2 == Int 3 == Int
4 == 3 -> 5
Substitution Map
1 : Int 2 : Int 3 : Int
4 : 3 -> 5
Canonical
Typed
Numbering
Generate equations
Unify
f (x + y) == "hello"
1
2
3
4
5
1 == Int 2 == Int 3 == Int
4 == 3 -> 5
Substitution Map
1 : Int 2 : Int 3 : Int
4 : Int -> 5
Canonical
Typed
Numbering
Generate equations
Unify
f (x + y) == "hello"
1
2
3
4
5
6
1 == Int 2 == Int 3 == Int
4 == 3 -> 5
Substitution Map
1 : Int 2 : Int 3 : Int
4 : Int -> 5
Canonical
Typed
Numbering
Generate equations
Unify
f (x + y) == "hello"
1
2
3
4
5
6
1 == Int 2 == Int 3 == Int
4 == 3 -> 5
6 == String
Substitution Map
1 : Int 2 : Int 3 : Int
4 : Int -> 5
6 : String
Canonical
Typed
Numbering
Generate equations
Unify
f (x + y) == "hello"
1
2
3
4
5
6
7
1 == Int 2 == Int 3 == Int
4 == 3 -> 5
6 == String
Substitution Map
1 : Int 2 : Int 3 : Int
4 : Int -> 5
6 : String
Canonical
Typed
Numbering
Generate equations
Unify
f (x + y) == "hello"
1
2
3
4
5
6
7
1 == Int 2 == Int 3 == Int
4 == 3 -> 5
6 == String
5 == 6 7 == Bool
Substitution Map
1 : Int 2 : Int 3 : Int
4 : Int -> 5
6 : String
Canonical
Typed
Numbering
Generate equations
Unify
f (x + y) == "hello"
1
2
3
4
5
6
7
1 == Int 2 == Int 3 == Int
4 == 3 -> 5
6 == String
5 == 6 7 == Bool
Substitution Map
1 : Int 2 : Int 3 : Int
4 : Int -> String
6 : String
7 : Bool
Ooooops!
Ooooops!
Did not check for multiple fields of the same name
Ooooops!
Did not check for multiple fields of the same name
{ name = "Sébastien"
, age = 42
, name = "Louis Auguste"
}
Ooooops!
- Do we patch our-self?
- Do we create a "good first issue"?
Ooooops!
- Do we patch our-self?
- Do we create a "good first issue"?
Conclusion
Look for
"good first issue"
Use data structures...
Even for tests!
Switch on the "unused variable" check
Use fuzzy tests
Find balance between:
-
fast development
-
include new people
Poka-Yoke
Mistakes
Avoid
What is elm?
Functional language
designed for front end development
and complex software
Sébastien Besnier
@_sebbes_
Compiler-Driven-Onboarding
By sebbes
Compiler-Driven-Onboarding
- 1,292