Change #2: Redesigned Initialization
Happy new year! I’ll continue the story…
Rupert and Jeremy are having lunch, and obviously they also talk about interfaces. Rupert tells Jeremy:
You know that I have functions like
intCounter : Int -> Counter
and
listCounter : Int -> Counter
which take a starting value and create a Counter
, appropriately backed by either an Int
or a List ()
. But I noticed that the users of these functions don’t really need the starting value, because they always create counters starting from zero, yet. So I was thinking whether I could remove this parameter. In the current function implementations, the parameter isn’t declared explicitly, but if it would be, I would have to pass it as the last parameter to the init
function of the Interface
module:
listCounter : Int -> Counter
listCounter start =
...
|> (\pipeline ->
IF.init (\raise n -> raise (List.repeat n ()))
pipeline
start
)
(I had to introduce the current value of the pipeline as an intermediate parameter.)
So I thought: what would I do, if I wanted to pass two or more parameters to the “initialization function” (the first parameter of IF.init
), or no parameter at all?
Jeremy smiles and answers:
You know what, Rupert? Right after we changed the Interface
module last time, I’ve been looking at the remaining code. The impl
function looks OK to me, but I had the feeling that the init
function could still be improved. Before I tell you what I’ve been thinking about, please do me a favour: could you briefly look at the usages of the init
function in your code? I suppose that the “initialization functions” you pass to the init
function all have the following shape:
- They create an initial value of the representation type. In my function signatures, it has the type
r
. - As the last step, they pass that value to the “raise” function of type
r -> t
, which I pass as the first parameter to your initialization functions.
After lunch, Rupert skims through their code.
intCounter : Int -> Counter
intCounter =
...
|> IF.init (\raise n -> raise n)
-- -
-- initial value of type Int: n
-- passed to "raise"
listCounter : Int -> Counter
listCounter =
...
|> IF.init (\raise n -> raise (List.repeat n ()))
-- ----------------
-- initial value of type List (): List.repeat n ()
-- passed to "raise"
fifo : List (Node state) -> Buffer state
fifo =
...
|> IF.init (\raise rep -> raise rep)
-- ---
-- initial value of type List (Node state): rep
-- passed to "raise"
shape bboxFn =
...
|> IF.init (\raise rep -> raise rep)
-- ---
-- initial value: rep
-- passed to "raise"
He calls Jeremy and tells him: You’re right. They all have the shape you supposed. How did you know that?
Jeremy answers:
The initialization functions, which you pass as the first parameter to the init
function, have to have the signature (r -> t) -> i -> t
(in my abstract type notation). This means that they have to return a value of type t
, in the counter example a value of type Counter
.
How can you create such a value? The only way to do this is by calling the magic “raise” function, which takes a value of the internal representation type r
. So, in your initialization functions, you first have to create this internal value and then pass it to the “raise” function.
After I recognized this, I knew that I could remove this last step (calling the “raise” function) from the user-supplied initialization functions and move it into the Interface
module. This would make the init
function look like
init : (i -> r) -> W r t t -> i -> t
init ir rtrt i =
let
rt : r -> t
rt r =
rtrt rt r
in
rt (ir i)
Now the initialization functions have the simpler type i -> r
. They don’t get the “raise” function anymore, and just have to create an r
value from the given i
value. I’ll then call the former “raise” function, internally called “rt”, in the init
function.
We can go even further: why should the init
function be responsible to take the i
value from the user and then directly pass it to the user-supplied initialization function? I don’t do anything else with the i
value. Why shouldn’t the user perform the ir i
call, which I do on the last line, in her own code and only give me the resulting r
value? This would reduce the init
function to
init : r -> W r t t -> t
init r rtrt =
let
rt : r -> t
rt r_ =
rtrt rt r_
in
rt r
An example for its usage:
listCounter : Int -> Counter
listCounter start =
impl Counter
...
|> init (List.repeat start ())
But why stop here? Syntactically, all the init
function does in the “impl |> init” pipeline is to get another parameter of type r
. If the impl
function would take this parameter itself, we wouldn’t need a pipeline at all! We could combine the code of the impl
and the init
functions into one.
Before:
impl : (o -> t) -> W r t o -> W r t t
impl ot rtro rt r =
ot (rtro rt r)
init : r -> W r t t -> t
init r rtrt =
let
rt : r -> t
rt r_ =
rtrt rt r_
in
rt r
After:
impl : (o -> t) -> W r t o -> r -> t
impl ot rtro r =
let
rtrt : W r t t
rtrt rt_ r_ =
ot (rtro rt_ r_)
rt : r -> t
rt r_ =
rtrt rt r_
in
rt r
If we inline the internal rtrt
function and remove the outer “r” parameter (to reduce shadowing), we get
impl : (o -> t) -> W r t o -> r -> t
impl ot rtro =
let
rt : r -> t
rt r =
(\rt_ r_ -> ot (rtro rt_ r_)) rt r
in
rt
We can simplify the last line in the internal rt
function to get the final version:
impl : (o -> t) -> W r t o -> r -> t
impl ot rtro =
let
rt : r -> t
rt r =
ot (rtro rt r)
in
rt
In fact, in the next version of the Interface
module, I’ll even remove the W
type alias, because in the meantime it is only used once. The signature of the impl
function then will be
impl : (o -> t) -> ((r -> t) -> r -> o) -> (r -> t)
Oh, by the way, Rupert, did you notice that we don’t have a value of type i
anymore in the signature? Coming back to your original question, this means that you are completely free to choose how you want to create the initial r
value. You can take one value from your user as before, two or more values, or no value at all. Here’s a version of your listCounter
function starting always at zero:
zeroListCounter : Counter
zeroListCounter =
IF.impl Counter
(\raise rep ->
{ up = \n -> raise (List.repeat n () ++ rep)
, down = \n -> raise (List.drop n rep)
, value = List.length rep
}
)
[]
Note how you now simply pass the initial []
value of type List ()
(representing the counter value “zero”) as the last parameter to the impl
function?
Rupert nods and says: it always feels extremely satisfying if you manage to improve your code just by removing parts of it, doesn’t it?
You can find the actual code in this Ellie. In the next part, we’ll be adding some code again…