Compiler-Driven-Onboarding

Open source in elm

https://slides.com/sebbes/compiler-driven-onboarding

Sébastien Besnier

@_sebbes_

What is elm?

Functional language
designed for front end development

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

https://stil4m.github.io/elm-analyse/

elm-analyze

A tool that allows you to analyse your Elm code, identify deficiencies and apply best practices.

https://stil4m.github.io/elm-analyse/

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

Compiler-Driven-Onboarding

Open source in elm

https://slides.com/sebbes/compiler-driven-onboarding

Sébastien Besnier

@_sebbes_

Compiler-Driven-Onboarding

By sebbes

Compiler-Driven-Onboarding

  • 1,292