Today I'm going to talk about a language feature that I think is missing from most languages. I know of at least one in which it exists, I do not know of any functional langauges in which it exists. The language feature I'm talking about is the removal of a name from the scope. Let me first talk about a situation that is ripe for bugs, and then introduce the idea of removing a name from the scope so that you do not use it mistakenly.
A common problem is using randomness in a functional language, I've written about this before as being a source of bugs. The reason is, that using a pseudo random number generator with a seed, is actually a really nice problem for having some self-contained state, ie. an object. Without that, you have to remember to store the updated seed back on your model, if you fail to do that, then you will at some point use the same iteration of the pseudo-random function. So you might have something like the following in your update
function in an Elm application:
1rollDice : Model -> Model
2rollDice =
3 let
4 generator =
5 Random.int 1 6
6 (newDice, newSeed )=
7 Random.step generator model.seed
8 in
9 { model
10 | dice = newDice
11 , seed = newSeed
12 }
13
14update : Msg -> Model -> ( Model, Cmd Msg)
15update message model =
16 ...
17 RollDice ->
18 ( rollDice model
19 , Cmd.none
20 )
21 ...
All great so far. Now suppose if the dice roll is a six you also want to double the player's score.
1update : Msg -> Model -> ( Model, Cmd Msg)
2update message model =
3 ...
4 RollDice ->
5 let
6 newModel =
7 rollDice model
8 in
9 ( case newModel.dice == 6 of
10 True ->
11 { newModel | score = newModel.score * 2 }
12 False ->
13 newModel
14 , Cmd.none
15 )
16 ...
Now notice, that the term newModel
is used four times, we can if you prefer refactor so that we always update the score, I think of this as 'restricting the scope of the conditional'
1update : Msg -> Model -> ( Model, Cmd Msg)
2update message model =
3 ...
4 RollDice ->
5 let
6 newModel =
7 rollDice model
8 newScore =
9 case newModel.dice == 6 of
10 True ->
11 newModel.score * 2
12 False ->
13 newModel.score
14 in
15 ( { newModel | score = newScore }
16 , Cmd.none
17 )
18 ...
This is arguably better, but the newModel
name is still used four times. Any one of those could be mistakenly written as model
. I find that after refactoring this is much more likely. For example, suppose on every roll of the dice your score is increased by one regardless, then only later is the new rule introduced which doubles the score if you roll a six. Note that in both instances of newModel.score
you could replace that with model.score
and you wouldn't fail any tests now. But if the requirements change again, this could well be a bug-in-waiting.
My whole point in this, is that directly after the binding of newModel
we would quite like to remove model
from use, that is, remove it from the scope. Any use after that point is probably a bug. It's possible that you do want to refer to the old model, but the language gives us no way to express that that is likely a bug.
In python, removing a name from the scope is actually possible using the del
keyword:
1>>> x = [1,2,3]
2>>> y = x
3>>> del x
4Traceback ...
5NameError: name 'x' is not defined
6>>> y
7[1, 2, 3]
A major problem with this idea in many functional languages, Elm included, is that the order of the definitions is not semantic. In particular the compiler is free to move them around if it sees fit. This means that it is somewhat dubious to talk about removing a name from the current scope since you would need to remove it from the entire scope, so you wouldn't be able to use it to create the new name. However, a way around this would be to have the entire rest of the expression in a new let
block, you could imagine some definition like this:
1update : Msg -> Model -> ( Model, Cmd Msg)
2update message model =
3 ...
4 RollDice ->
5 let
6 newModel =
7 rollDice model
8 scoredModel =
9 let without model
10 newScore =
11 case newModel.dice == 6 of
12 True ->
13 newModel.score * 2
14 False ->
15 newModel.score
16 in
17 { newModel | score = newScore }
18 in
19 ( scoredModel
20 , Cmd.none
21 )
22 ...
Whereby a use of model
within the inner let
that uses an imaginary syntax to remove model
from the scope let without model
would be an error. If Elm allowed you to re-define a name you could somewhat fake this by doing:
1update : Msg -> Model -> ( Model, Cmd Msg)
2update message model =
3 ...
4 RollDice ->
5 let
6 newModel =
7 rollDice model
8 scoredModel =
9 let
10 model = ()
11 newScore =
12 case newModel.dice == 6 of
13 True ->
14 newModel.score * 2
15 False ->
16 newModel.score
17 in
18 { newModel | score = newScore }
19 in
20 ( scoredModel
21 , Cmd.none
22 )
23 ...
In that case, any use of model
without the inner scoped let
would likely be a type-error since model
has been re-declared to be the unit-value. This would certainly prevent you from using it in any of the positions newModel
is used here.
Still though, both with the imaginary let without
syntax, and with the (disallowed) rebinding, this all feels rather clunky. I cannot come up with a nice syntax for expressing the fact that a name should not be used further in a let binding. Although functional let definitions do tend to be unordered, they are still read in by the compiler in the order they are written so in theory we could still have a construct to hide a name in the remainder of the bindings in a let
. So I could imagine writing something like the following, whereby hide <name>
syntax means hide the given name from the remaining declarations and the in-expression:
1update : Msg -> Model -> ( Model, Cmd Msg)
2update message model =
3 ...
4 RollDice ->
5 let
6 newModel =
7 rollDice model
8
9 hide model
10
11 newScore =
12 case newModel.dice == 6 of
13 True ->
14 newModel.score * 2
15 False ->
16 newModel.score
17 in
18 ( { newModel | score = newScore }
19 , Cmd.none
20 )
21 ...
Ultimately I think this feature is just too awkward to implement in any elegant language. It wouldn't be used very often, probably not as often as it should be. So likely the bother of complicating the language is just not worth the extra expressivity. Unless of course someone else comes up with a really elegant syntax.