{{ message }}

Instantly share code, notes, and snippets.

choonkeat/destructuring-phantom-type-appendix.md

Last active Feb 18, 2020

A note about "type signature" and "type variables"

convert : Exchange from to -> Currency from -> Currency to

from and to are type variables to ensure things line up.

They are not variables in the regular sense: we cannot use from or to in our function body to + or do anything with. Actually, in our function body, there is no variable from or to

convert : Exchange from to -> Currency from -> Currency to
convert arg1 arg2 =
-- FYI you can use variable `arg1` or `arg2`
-- BUT there are no variables `from` or `to` for use here

While reading https://thoughtbot.com/blog/modeling-currency-in-elm-using-phantom-types#constraint-2-conversions there's a problem understanding the type signature of

convert : Exchange from to -> Currency from -> Currency to
convert (Exchange rate) (Currency c) =
Currency <| round (rate * toFloat c)

I [mentally] map Exchange from to in first line to Exchange rate in second line. Then I am lost how do I get the rate and where do from to go? Could someone give suggestion on how to understand this?

Rewriting the code a bit, I think this type signature won't be confusing:

convert : Exchange from to -> Currency from -> Currency to
convert exchangeFromTo currencyFrom =

So the first argument exchangeFromTo argument variable is type Exchange from to (and currencyFrom second argument is type Currency from)

Now to work with exchangeFromTo, we need to pattern match / destructure it to get the Float value we want out of it. To know what patterns are available to match with in a Custom Type, we can look at the type definition:

type Exchange from to
= Exchange Float -- this is our only constructor, `Exchange blah` where `blah` will be a `Float` value

and we can use it like this

convert : Exchange from to -> Currency from -> Currency to
convert exchangeFromTo currencyFrom =
case exchangeFromTo of
Exchange rate ->
-- do stuff with variable `rate` which is a `Float`

When there's only 1 pattern, we can also write it in this way (see here for more examples)

convert : Exchange from to -> Currency from -> Currency to
convert exchangeFromTo currencyFrom =
let
(Exchange rate) = exchangeFromTo
in
-- at this point, we can use the variable `rate` of `Float` type

since (Exchange rate) is equal to exchangeFromTo, we can replace it

convert : Exchange from to -> Currency from -> Currency to
convert (Exchange rate) currencyFrom =

So now we've arrived at where you were confused, the first argument being specified as (Exchange rate) and the rest of the function using variable rate value of Float type

ingemar0720 commented Feb 17, 2020

What if there are more constructor in this example?, e.g. to re-write type Exchange to be

type Exchange from to
= Exchange Float
= Exchange String String

Shall the implementation also add in one argument?

convert : Exchange from to -> Currency from -> Currency to
convert (Exchange rate someInt) currencyFrom =

Or we shall use case of to do pattern match?

convert exchangeFromTo  currencyFrom =
case exchangeFromTo of
Exchange rate ->
-- do stuff with variable `rate` which is a `Float`
Exchange rate someInt ->
-- do stuff with variable `rate` which is a `Float` and variable`someInt` which is a `Int`

choonkeat commented Feb 17, 2020 • edited

in some languages (Haskell, Erlang, Elixir) you can define multiple function bodies to cover all the patterns

convert (Exchange rate) currencyFrom =
-- do stuff with `rate`

convert (Exchange rate someInt) currencyFrom =
-- do stuff with `rate`, `someInt`

Elm does not allow that, preferring you explicitly express all the variants in one place with the usual case ... of in 1 function body

convert exchangeFromTo currencyFrom =
case exchangeFromTo of
Exchange rate ->
-- ...
Exchange rate someInt ->
-- do stuff with `rate`, `someInt`

The ability to define multiple function bodies for pattern matching is pretty cool and actually very very mathy. For example, here's factorial (one function matches the exact value 0 another function matches anything else as n)

factorial 0 = 1
factorial n = n * factorial (n - 1)

But I agree with Elm approach for maintainability (don't worry about locating the various functions, missing out on a pattern and making micro decisions on where to put that other function, tracing down the wrong function when another is more matching...)

factorial n =
case n of
0 -> 1
n -> n * factorial (n - 1)