Demystifying Jeremy's Interfaces

Maybe this is known and just awaiting prose, but the sense left in the last few messages isn’t clear.

You are pre-computing the doubled object which in turn has a doubled-object to pre-compute, etc. The interface should have a function from unit to the doubled object and the top-level Thing wrapper can have a function which hides. (If Elm 0.19 had not removed lazy values, you could put a lazy object into the interface and force it in the top-level wrapper to avoid recomputes.)

Hi Mark, thank you for your remark. Nice to see your name again.

The next parts of this series are in the works, but unfortunately I have a lot of other things to do currently, so it will take me a few more days to publish the next part. Sorry about that.

Your post not only contained a hint how to deal with the last problem, but also gave me a few more days before this topic will be closed :smile: :+1:

Change #4: Add Implicit Laziness

This is one of the last parts of the story, I promise…


In the meantime, Rupert’s colleague Pit finally wants to understand the interface technology Rupert keeps talking about. As always, he starts to do so by modifying the existing code.

He adds a reset operation to the CounterOperations record, meaning to set the counter value to zero:

type alias CounterOperations t =
    { up : Int -> t
    , down : Int -> t
    , value : Int
    , reset : t
    }

and adds a utility function for the Counter users:

reset : Counter -> Counter
reset (Counter counterOperations) =
    counterOperations.reset

He handles the new operation in the map-like counterOps function:

counterOps : (rep -> typ) -> CounterOperations rep -> CounterOperations typ
counterOps raise ops =
    { up = ops.up >> raise
    , down = ops.down >> raise
    , value = ops.value
    , reset = ops.reset |> raise
    }

ops.reset is a value of type rep, so he has to pipe the value with |> into the raise function, rather than using the function composition operator >> as in the up and down operations.

It’s easy for him to add the new operation to both the Int and List () counter implementations:

counterImplInt : Int -> CounterOperations Int
counterImplInt rep =
    { up = \n -> rep + n
    , down = \n -> rep - n
    , value = rep
    , reset = 0
    }


counterImplList : List () -> CounterOperations (List ())
counterImplList rep =
    { up = \n -> List.repeat n () ++ rep
    , down = \n -> List.drop n rep
    , value = List.length rep
    , reset = []
    }

The counter value “zero” is represented by the integer value 0 or by the empty list [], respectively.

Everything compiles, but trying to create a new Counter he immediately gets an error:

Initialization Error

RangeError: Out of memory


Since he has no idea what could be wrong, he asks Rupert for help. Rupert hasn’t seen this error in the context of interfaces before, but it looks like an endless recursion to him.

Unfortunately, Jeremy is visiting the type system demons and wizards conference and therefore isn’t available. Rupert and Pit have to help themselves.

It’s clear that the culprit must be the new reset operation, because without it everything works fine. Rupert and Pit try the example with both versions of Jeremy’s magic function they have seen before, but both versions exhibit the same behavior.

What :interrobang: happens upon creating an instance of the modified Counter interface?


Rupert suggests:

Let’s write down the steps of the evaluation.


Pit

I wouldn’t be able to do this, but if you can do it… May be we’ll see where the problem is. How do you want to proceed?


Rupert

Let’s remove all the code which isn’t relevant to the problem. Let’s use a Counter type with just a single reset function.


Rupert chooses the magic impl function from the fifth post (because the code is shorter):

impl : (ops -> typ) -> ((rep -> typ) -> rep -> ops) -> (rep -> typ)
impl constructor map =
    let
        raise : rep -> typ
        raise rep =
            constructor (map raise rep)
    in
    raise


type Counter
    = Counter CounterOperations


type alias CounterOperations =
    { reset : Counter }


intCounter : Int -> Counter
intCounter start =
    impl Counter (\raise rep -> { reset = raise 0 }) start


myCounter : Counter
myCounter =
    intCounter 12

He starts to write the following evaluation steps:

Creating the Counter:

  1. intCounter 12

Using the body of intCounter:

  1. impl Counter (\raise rep -> { reset = raise 0 }) 12

The body of impl:

  1. raise 12

Definition of raise:

  1. Counter ((\raise rep -> { reset = raise 0 }) raise 12)

Evaluation of the function call:

  1. Counter { reset = raise 0 }

Definition of raise:

  1. Counter { reset = Counter ((\raise rep -> { reset = raise 0 }) raise 0) }

Evaluation of the function call:

  1. Counter { reset = Counter { reset = raise 0 } }

Definition of raise:

  1. Counter { reset = Counter { reset = Counter ((\raise rep -> { reset = raise 0 }) raise 0) } }

Evaluation of the function call:

  1. Counter { reset = Counter { reset = Counter { reset = raise 0 } } }

Pit

Oh, I can see the recursion! If we look at steps 5, 7, and 9:

Counter { reset = raise 0 }

Counter { reset = Counter { reset = raise 0 } }

Counter { reset = Counter { reset = Counter { reset = raise 0 } } }

it’s clear that this never ends. Elm tries to create an endlessly nested structure. But why didn’t it happen before?


Rupert

I think I understand now. Just as an exercise: why don’t you write down the steps for a version of Counter where the interface just has a single up operation?


Pit

Having you on my side, I can try it.


He more or less copies Rupert’s steps from aboove:

Creating the Counter:

  1. intCounter 12

Using the body of intCounter, now with an up operation:

  1. impl Counter (\raise rep -> { up = \n -> raise (rep + n) }) 12

The body of impl:

  1. raise 12

Definition of raise:

  1. Counter ((\raise rep -> { up = \n -> raise (rep + n) }) raise 12)

Evaluation of the function call:

  1. Counter { up = \n -> raise (12 + n) }

Definition of raise:


Rupert

Stop! We don’t need more steps. Elm stops the evaluation right here. We now have a record where the up field is a function. This function is only evaluated when it is called.


Pit

OK, but don’t we have a function in the reset case, too?

  1. Counter { reset = raise 0 }

Rupert

For reset, we have a function application, or a function call. For up, we have a function definition. A function call can be evaluated immediately, if the arguments of the function call are constant, as they are in the reset case.


Pit

I see. So this is the difference between the up and the reset operations?


Rupert

Yes, I think this is it. Do you have an idea now what we could try to make the reset case work?


Pit

Hmm. From what you said, we should try to use a function definition for reset, too. But how do we do this?


Rupert

Do you know the lazy functions in the Json.Decode and Parser modules? They use a function of type () -> ..., sometimes called a “thunk”, to prevent endless recursions. You could do the same for the reset operation.


Pit changes his code accordingly…

He modifies the reset operation in the CounterOperations record to be a function:

type alias CounterOperations t =
    { up : Int -> t
    , down : Int -> t
    , value : Int
    , reset : () -> t
    }

The utility function for the Counter users has to be changed, too:

reset : Counter -> Counter
reset (Counter counterOperations) =
    counterOperations.reset ()

In the map-like counterOps function, he uses the function composition operator in the reset field, too, because now ops.reset is a function:

counterOps : (rep -> typ) -> CounterOperations rep -> CounterOperations typ
counterOps raise ops =
    { up = ops.up >> raise
    , down = ops.down >> raise
    , value = ops.value
    , reset = ops.reset >> raise
    }

Finally the Int and List () counter implementations:

counterImplInt : Int -> CounterOperations Int
counterImplInt rep =
    { up = \n -> rep + n
    , down = \n -> rep - n
    , value = rep
    , reset = \() -> 0
    }


counterImplList : List () -> CounterOperations (List ())
counterImplList rep =
    { up = \n -> List.repeat n () ++ rep
    , down = \n -> List.drop n rep
    , value = List.length rep
    , reset = \() -> []
    }

Fingers crossed, they try the new code, and… it works! Success!

:+1:


A few days later, Jeremy returns from the demons and wizards conference. Rupert tells him about Pit’s problem and shows him the new code.


Jeremy

Yeah, I’ve already heard of your experiment. Congratulations that you were able to fix the problem on your own!

I have been thinking about it. Are you satisfied with the solution?


Rupert

Hmm, I thought that I am, but if you ask me like that… Can the code be improved, again?


Jeremy

Do you remember when we separated the interface definition from the implementation in order to hide the raise function from the interface implementations?


Rupert

Of course I do. Do you think that we can hide the laziness from the implementations, too?


Jeremy

Not only that, we can hide it even from the counterOps function.

But we have to change other things, above all the function in the Interface module. Have you decided which name the function should have, finally?


Rupert

You mean your “magic” function? Currently I tend to name it make or create because I like create Counter ...


Jeremy

OK. We’ll name it create.

In order to hide the laziness, we have to add the additional () parameter to the definition of the raise function. This makes the create function look like this:

create : (opsTyp -> typ) -> ((rep -> () -> typ) -> opsRep -> opsTyp) -> (rep -> opsRep) -> (rep -> typ)
create constructor mapOps impl rep =
    let
        raise : rep -> () -> typ
        raise rep_ () =
            constructor (mapOps raise (impl rep_))
    in
    raise rep ()

Can you see why this works?


Rupert

Hmm. It looks a little bit different than the “normal” lazy functions. You added the () as the second parameter to the raise function, not as the first.


Jeremy

Yes. In order to have the desired effect, in the end it is necessary to have a partially applied function call where only the () argument is missing.

We call raise in the counterOps function and pass the current internal state called “rep” into it. Thus, the missing () argument has to come after the “rep” parameter.

With this change, each raise call in the counterOps function is missing the final () argument. Evaluation is guaranteed to stop there.


Rupert

I see. Just to be sure, can you show me the new counterOps function?


Jeremy

Of course. Here it is:

counterOps : (rep -> typ) -> CounterOperations rep -> CounterOperations typ
counterOps raise ops =
    { up = ops.up >> raise
    , down = ops.down >> raise
    , value = ops.value
    , reset = ops.reset |> raise
    }

Rupert

Wait - this looks exactly like Pit’s original code, including the forward pipe operator and even the function signature! How can this work? Don’t we have to change the function signature at least, because we changed the type of the raise function?


Jeremy

You are right. I cheated a little bit. To be precise, the function signature should be

counterOps : (rep -> () -> typ) -> CounterOperations rep -> CounterOperations (() -> typ)

As you can see from this version of the function signature, we also have to change the main interface type to

type Counter
    = Counter (CounterOperations (() -> Counter))

But if we want to, we can totally hide this from the counterOps function by generalizing the type () -> typ to simply typ. It still compiles, because it’s still just a map function from CounterOperations a to CounterOperations b.


Rupert

Magic :sparkles:


Jeremy

Oh, it’s more a little bit of cheating than magic here.

Because of the change in Counter, we have to change the utility functions, too, those which return a new Counter:

up : Int -> Counter -> Counter
up n (Counter counterOperations) =
    counterOperations.up n ()


down : Int -> Counter -> Counter
down n (Counter counterOperations) =
    counterOperations.down n ()


reset : Counter -> Counter
reset (Counter counterOperations) =
    counterOperations.reset ()

But besides that, the implicit laziness is completely hidden from the remaining code.


Rupert

I stand by my opinion: it’s magic :sparkles:


If you want to try the magic for yourself, here’s an Ellie with implicit laziness.

3 Likes

So, you boiled the whole thing down to basically this:

create : (opsTyp -> typ) -> ((rep -> () -> typ) -> opsRep -> opsTyp) -> (rep -> opsRep) -> (rep -> typ)
create constructor mapOps impl rep =
    let
        raise : rep -> () -> typ
        raise rep_ () =
            constructor (mapOps raise (impl rep_))
    in
    raise rep ()

Very clever :clap:

1 Like

This part could be code generated (by say an elm-review rule):

counterOps : (rep -> typ) -> (CounterOperations rep -> CounterOperations typ)
counterOps raise ops =
    { up = ops.up >> raise
    , down = ops.down >> raise
    , value = ops.value
    , reset = ops.reset |> raise
    }

Before I started to write all this I briefly talked to Jeremy (the real one :slightly_smiling_face:) about a couple of things, and he had functions/operations which returned for example a tuple of ( model, Cmd msg ) where the model was the thing which needed to be raised, so it might be more complicated in general than in my simple examples, but I, too, think the raising code (the map function) could be generated.

As always: thank you Rupert for your comments!

Yes, I realised after my comment about the codegen, that the patterns could be more complex that just the three variants in this example.

I also experimented with a (model, Cmd Msg) type structure. Works but you need a global Msg type accross all the object-oriented variants. An alternative might be to do something with (model, Cmd Value), but with the overhead of having to write codecs for Value for all events in the system. Gives you “sealed” TEA components like web components, but perhaps that is not really so useful.

This pattern is useful for data structures (like your counters). I would be interested benchmarking it versus the same implementation without the interface hiding the implementation type.

Also useful I have found for creating extensible systems in an OO way within Elm. I shall try and convert my elm-oo-style code to your single create interface function and see how I get on.

Intermezzo #2: Back to Real Laziness

OK. We’re back again in the real world. Let’s see whether my Thing example works with the implicit laziness variant…


I have to add laziness to the interface type:

type Thing
    = Thing (ThingOperations (() -> Thing))

The convenience function returning a Thing has to be changed, too:

thingDouble : Thing -> Thing
thingDouble (Thing ops) =
    ops.double ()

If I got it right, then that should be all that’s needed to use the lazy variant of the Interface module. Can this be true?


Again, I start elm reactor, navigate to the source file with my little example program …

myListOfThings : List Thing
myListOfThings =
    [ aString "one", anInt 1, anInt 12, aBool True ]


main : Html msg
main =
    myListOfThings
        |> List.map thingSize
        |> Debug.toString
        |> Html.text

… and get:

[3,1,4,1]

Success! I really can create a list of different Things now without getting into an endless recursion!


Let’s see if the problematic double operation works, too:

main : Html msg
main =
    myListOfThings
        |> List.map thingDouble
        |> List.map thingSize
        |> Debug.toString
        |> Html.text

I reload the page and get

[6,2,5,0]

It works! If you want to try it on your own, here’s an Ellie with the code.


To be honest, I don’t know whether I can use the interface technology in my own Elm code… yet. But it’s good to have it available as another tool.

While thinking about possible use cases, I had an idea I’d like to explore in one more part of the story. After that, a little recap with the three variants of the “magic” function we encountered so far, and then we’re finally done.

Stay tuned…

1 Like

Thought Experiment: Interfaces in APIs

We’re still in story-telling mode…


Rupert is thinking about new use cases for the interface technology. The technology itself can be used to hide an internal type behind an interface. But we can make the interface type itself opaque and hide the usage of the whole interface technology behind a module API.

Rupert wants to try this with a real example, and it doesn’t take him long to implement something. He shows the new module to his colleague Pit.


Rupert

Hey Pit, let me show you my new “BitShifter” service! Here’s the module definition:

module BitShifter exposing
    ( BitShiftable, Operations
    , shiftLeftBy, shiftRightBy, toBitShiftable
    )

It lets you turn any type you like into a BitShiftable, a value that you can then shift left and shift right, just like the bit shift methods in the Bitwise core module.

The type BitShiftable is opaque. You don’t need to know the exact implementation in order to use it. Such a value can be shifted bitwise with the functions

shiftLeftBy : Int -> BitShiftable -> BitShiftable

shiftRightBy : Int -> BitShiftable -> BitShiftable

Isn’t this great?

You just have to tell the service a little bit about your type. The Operations record specifies what the service wants to know about your type in order to be able to to turn it into a BitShiftable:

type alias Operations a =
    { up : Int -> a
    , down : Int -> a
    , value : Int
    }

toBitShiftable : (a -> Operations a) -> a -> BitShiftable

So if you tell me how to create the up, down, and value fields for any value of your type a, plus a concrete a value, I can turn the latter into a BitShiftable!


Pit

Hey, this is nice, Rupert! But you can’t trick me :grinning: The Operations record told me your secret: you are using an interface behind the scenes, aren’t you?


Rupert

Hmm…


Pit

No problem. Let’s pretend I don’t know. I’ll test your service right away with my favorite type: a list of units…

Let me see… I’ll give you the operations and you turn my List () into a BitShiftable


He writes:

myBitShiftable : BitShifter.BitShiftable
myBitShiftable =
    BitShifter.toBitShiftable
        (\list ->
            { up = \n -> List.repeat n () ++ list
            , down = \n -> List.drop n list
            , value = List.length list
            }
        )
        [ (), (), () ]

Pit

And now I can shift it left and right, right?

shifted : BitShifter.BitShiftable
shifted =
    myBitShiftable
        |> BitShifter.shiftLeftBy 2
        |> BitShifter.shiftRightBy 1

Nice! But what can I do with the shifted result? Can I get it back as a List () to be able to look at it? What about a function like

fromBitShiftable : BitShiftable -> a

Rupert, who is a little disappointed about the course of the experiment, doesn’t want to give up yet, so he just says:

A function with this signature wouldn’t be possible, but let me think about it…


Of course he has been using the interface technology behind the scenes. Here’s his code:

type BitShiftable
    = BitShiftable (Operations BitShiftable)


type alias Operations a =
    { up : Int -> a
    , down : Int -> a
    , value : Int
    }


bitShiftableOps : (rep -> typ) -> Operations rep -> Operations typ
bitShiftableOps raise ops =
    { up = ops.up >> raise
    , down = ops.down >> raise
    , value = ops.value
    }


toBitShiftable : (a -> Operations a) -> a -> BitShiftable
toBitShiftable impl =
    IF.create BitShiftable bitShiftableOps impl


shiftLeftBy : Int -> BitShiftable -> BitShiftable
shiftLeftBy n (BitShiftable ops) =
    ops.up (ops.value * (2 ^ max n 0) - ops.value)


shiftRightBy : Int -> BitShiftable -> BitShiftable
shiftRightBy n (BitShiftable ops) =
    ops.down (ops.value - (ops.value // (2 ^ max n 0)))

Nothing special so far. He even used the same operations as in the Counter interface to implement the bit shift operations. It’s no surprise that even Pit was able to see through the secret.


When he said that the fromBitShiftable function from above wouldn’t be possible, he was right. What type should the function result be? It can’t simply be an a like Pit suggested, because the a could be anything.

But Rupert has an idea: what if the BitShiftable type had a type parameter? A function like this would be possible to implement in Elm:

fromBitShiftable : BitShiftable a -> a

On the other hand, this would mean that the underlying type wouldn’t be hidden anymore, so it wouldn’t be possible to create a list of different BitShiftables anymore. For Rupert’s BitShifter service, this wouldn’t be a problem. The goal of the service is to enable bit-shifting-like operations for a user-supplied type, not to make multiple different BitShiftable values combinable.

Great. Rupert has a plan how to proceed.


So how could he implement the fromBitShiftable function? He starts with a version that looks like the value function from the Counter interface:

fromBitShiftable : BitShiftable a -> a
fromBitShiftable (BitShiftable ops) =
    ops.internal

OK. So he needs an Operations record with an additional internal field:

type alias Operations a =
    { up : Int -> a
    , down : Int -> a
    , value : Int
    , internal : a
    }

The new field has to be dealt with in the map-like bitShiftableOps function:

bitShiftableOps : (rep -> typ) -> Operations rep -> Operations typ
bitShiftableOps raise ops =
    { up = ops.up >> raise
    , down = ops.down >> raise
    , value = ops.value
    , internal = ops.internal
    }

Hmm. This doesn’t compile. To make the compiler happy, Rupert would have to raise the internal value, too. But he doesn’t want to change the type of the internal value.

Rupert knows how to deal with this problem: he has to introduce an additional type parameter to the Operations record type, too:

type alias Operations i a =
    { up : Int -> a
    , down : Int -> a
    , value : Int
    , internal : i
    }

Now he can leave the internal value as it is without having to apply the raise function:

bitShiftableOps : (rep -> typ) -> Operations rep rep -> Operations rep typ
bitShiftableOps raise ops =
    { up = ops.up >> raise
    , down = ops.down >> raise
    , value = ops.value
    , internal = ops.internal
    }

This compiles! The BitShiftable type now looks like this:

type BitShiftable a
    = BitShiftable (Operations a (BitShiftable a))

Great! Now the toBitShiftable function:

toBitShiftable : (a -> Operations a a) -> a -> BitShiftable a
toBitShiftable impl =
    IF.create BitShiftable bitShiftableOps impl

Only the function signature had to be changed. Nice.


But Rupert knows: if I show Pit this new version, he’ll still be able to figure out what I’ve done just by looking at the types of the toBitShiftable function.

Wouldn’t it be nice if Rupert could use the same Operations record as before in his BitShifter API?

Here’s how the user-supplied impl parameter looked like in the old and in the new version:

old:
    a ->
        { up : Int -> a
        , down : Int -> a
        , value : Int
        }


new:
    a ->
        { up : Int -> a
        , down : Int -> a
        , value : Int
        , internal : a
        }

Wait… Rupert is sure that he can turn the former into the latter!


He defines two different operations record types: one which is externally visible and an internal one:

type alias Operations a =
    { up : Int -> a
    , down : Int -> a
    , value : Int
    }


type alias InternalOperations i a =
    { up : Int -> a
    , down : Int -> a
    , value : Int
    , internal : i
    }

Now he can convert the original impl parameter with the Operations record into a version using the new InternalOperations record:

internalImpl : (a -> Operations a) -> a -> InternalOperations a a
internalImpl impl a =
    { up = (impl a).up
    , down = (impl a).down
    , value = (impl a).value
    , internal = a
    }

Using this helper function, we get almost the original version of the toBitShiftable function:

toBitShiftable : (a -> Operations a) -> a -> BitShiftable a
toBitShiftable impl =
    IF.create BitShiftable bitShiftableOps (internalImpl impl)

After all, this was easy!


Rupert shows Pit the new version of the BitShifter module.

Rupert

Hi Pit. Do you remember the fromBitShiftable function you wanted to have to be able to get back at the shifted value?

I told you that it wouldn’t be possible to implement it in the way you suggested. But if we add a type variable to the BitShiftable type, then I can give you a function

fromBitShiftable : BitShiftable a -> a

Pit

Hmm, OK, so what would I have to change in my code?


Rupert

As I said: you have to add a type parameter.


Pit

Wait. That’s all I need to do? Everything else stays the same? If this is true, then you’re slowly but surely turning into a type system wizard like Jeremy!

Let me try it…


He adds the type parameter to his code:

myBitShiftable : BitShifter.BitShiftable (List ())
myBitShiftable =
    BitShifter.toBitShiftable
        (\list ->
            { up = \n -> List.repeat n () ++ list
            , down = \n -> List.drop n list
            , value = List.length list
            }
        )
        [ (), (), () ]


shifted : BitShifter.BitShiftable (List ())
shifted =
    myBitShiftable
        |> BitShifter.shiftLeftBy 2
        |> BitShifter.shiftRightBy 1

Pit

And now I can get back the original and the bit-shifted value?


He types:

myBitShiftable
    |> BitShifter.fromBitShiftable
--> [(), (), ()]


shifted
    |> BitShifter.fromBitShiftable
--> [(), (), (), (), (), ()]

Pit

Jeremy, um, I mean, Rupert, you are a genius!

I can’t wait until you explain your wizardry to me!


Rupert smiles. Mission completed.

-- ::: --

Ok, this was the last aspect of the interface technology I wanted to look at. I found it interesting that you can hide a set of user-supplied functions behind an easy-to-use interface type, and that you can hide them from both the users of a module as well as from the module code itself.

In the BitShifter example, both the module user as well as other module functions can use this simple interface:

type BitShiftable a

shiftLeftBy : Int -> BitShiftable a -> BitShiftable a

shiftRightBy : Int -> BitShiftable a -> BitShiftable a

In the next days, I’ll add a short recap with the three versions of Jeremy’s magic interface functions we’ve seen before, … and then one last thing.

I think the goal of using interfaces is to hide the implementation. This allows you to program in Elm in a more open-ended way. New implementations can be added at a later time without changing existing code. I think as soon you want functions like this that recover the implementation type, it renders the technique a bit pointless. This is just going back to the “poor mans typeclass” approach.

Not that it isn’t without its uses. Consider List.map or other higher order utility functions:

map : (a -> b) -> List a -> List b

Separates the iteration over the list part from the mapping function. Its higher order, so the caller provides the mapping function, which is one of the more obvious ways in which Elm or functional programming in general allows implementation details to be hidden or deferred. In Elm the other obvious ways are modules and opaque types, and the package system which allows internal modules to not be exposed.

Hi Rupert, thank you for your response.

I think the goal of using interfaces is to hide the implementation.

Yeah, maybe this wasn’t a good idea after all, but I found it interesting, so I added one more episode. I currently haven’t much time, so it will still take me some days to finish this series.

So with ops container, we would have

interface : (ops typ -> typ) -> ((rep -> typ) -> ops rep -> ops typ) -> (rep -> ops rep) -> (rep -> typ)

This part suspiciously looks like refold from recursion-scheme.

refold :: Functor f => (f b -> b) -> (a -> f a) -> a -> b 

Replace f with ops, a with rep, b with typ and you get

refold :: Functor ops => (ops typ -> typ) -> (rep -> ops rep) -> (rep -> typ)

Since Functor ops is effectively (a -> b) -> (f a -> f b), this refold is a slightly more general version of interface.
If Elm had ability to derive map function (Would not need typeclass for this), boilerplate for implementing this part could be condensed.

Interesting how a function analogous to refold could be used for hiding implementation.

By the way, the technique of constructing a data recursively is called “Typing the knot”. The object constructor (rep -> typ) is recursively referenced to attain the correct operation type for typ.

3 Likes

Previously I tried out this interface idea on a little spike project: GitHub - rupertlssmith/elm-oo-style: An example showing how to do OO style programming in Elm. I used the original interfaces as presented by Jeremy for this. Was generally a pretty good experience working this way, and allowed me to simplify parts of the code that I ported this from.

Now I am going to work on a real drawing application, and try to flip-it-round to the OO style. The reason is that we keep on adding more and more possible things into the drawing, so expressing this as an interface will make it more easily extensible. Feels like the right place for this idea.

This time though, I am going to try working with the reduced combinator you figured out Pit:

create : (opsTyp -> typ) -> ((rep -> () -> typ) -> opsRep -> opsTyp) -> (rep -> opsRep) -> (rep -> typ)
create constructor mapOps impl rep =
    let
        raise : rep -> () -> typ
        raise rep_ () =
            constructor (mapOps raise (impl rep_))
    in
    raise rep ()

I’m sure you’ll write about your findings.

If performance matters to the drawing application: I wrote a little benchmark using the Counter example comparing Jeremy’s original version [A] with the 3 variants I showed here (rearranged code) [B], here (simpler interface implementations) [C] and here (implicit laziness) [D]. The results were:

[B] faster than [C and D] faster than [A]

Are you able to share your benchmark? It would also be worth comparing with the simplest counter of all - just an Int, to get an idea of what overhead is added by this technique.

For my drawing program, performance will not be affected noticeably. The reason I am confident of that, is that updates to the drawing will be made only in response to user input. Even if the user is dragging the mouse, this will amount to only a few hundred events per second max. I really do not think any overhead will be noticeable at these kind of rates.

The story might be different though if you used this technique to implement a data structure. For example, if we made an interface for Dict, allowing multiple implementations of it. We could wrpa the elm/core Dict in this interface. If we then did something like a foldl over this Dict with say 1 million elements and compared with the bare elm/core Dict, we might expect to see some slow down. It would certainly be interesting to investigate this.

Hey Abastro, this is amazing!

There was a time when @hayleigh was praising recursion schemes on a regular basis in the #language-implementation channel of the Elm Slack. Back then I briefly skimmed through the code and the samples. I remember that there were functions with esoteric names like cata, para, zygo, and even an optimized version of an unfold followed by a fold. I didn’t know that there are more approachable names for these functions like “fold”, “unfold”, and “refold”, and I would have never ever noticed that one version of the interface create function is the same as refold (modulo type classes)!

This is fascinating! :star_struck: Thank you for noticing this and writing about it!

Spoiler: this isn’t the last time that @hayleigh and Haskell will be mentioned in this series…

1 Like

Here’s an Ellie with the benchmark: https://ellie-app.com/mcnQB8prvgJa1

It takes some time to run, so be patient…

1 Like

The next posts will be the last part of this (already way too long) series:
a short recap of Jeremy’s original code and the 3 variants I discussed earlier.


The Original Version


Types

type Counter
    = Counter CounterOperations


type alias CounterOperations =
    { up : Int -> Counter
    , down : Int -> Counter
    , value : Int
    , reset : () -> Counter
    }
  • simple types
  • explicit thunk in reset operation

Utility Functions

up : Int -> Counter -> Counter
up n (Counter counterOperations) =
    counterOperations.up n


down : Int -> Counter -> Counter
down n (Counter counterOperations) =
    counterOperations.down n


value : Counter -> Int
value (Counter counterOperations) =
    counterOperations.value


reset : Counter -> Counter
reset (Counter counterOperations) =
    counterOperations.reset ()
  • additional () parameter in reset function

Implementation and Instance Creation

intCounter : Int -> Counter
intCounter =
    IF.impl CounterOperations
        |> IF.wrap (\raise rep n -> rep + n |> raise)
        |> IF.wrap (\raise rep n -> rep - n |> raise)
        |> IF.add identity
        |> IF.wrap (\raise _ () -> 0 |> raise)
        |> IF.map Counter
        |> IF.init (\raise n -> n |> raise)


listCounter : Int -> Counter
listCounter =
    IF.impl CounterOperations
        |> IF.wrap (\raise rep n -> List.repeat n () ++ rep |> raise)
        |> IF.wrap (\raise rep n -> List.drop n rep |> raise)
        |> IF.add List.length
        |> IF.wrap (\raise _ () -> [] |> raise)
        |> IF.map Counter
        |> IF.init (\raise n -> List.repeat n () |> raise)
  • pipeline style using record constructor function
  • every implementation has to apply the raise function in IF.wrap
  • every implementation has to account for laziness in the reset function
  • initialization with IF.init requires exactly one parameter

Interface Module

impl : t -> (raise -> rep -> t)
impl constructor =
    \_ _ -> constructor


wrap :
    (raise -> rep -> t)
    -> (raise -> rep -> (t -> q))
    -> (raise -> rep -> q)
wrap method pipeline =
    \raise rep -> method raise rep |> pipeline raise rep


add :
    (rep -> t)
    -> (raise -> rep -> (t -> q))
    -> (raise -> rep -> q)
add method pipeline =
    \raise rep -> method rep |> pipeline raise rep


map : (a -> b) -> (raise -> rep -> a) -> (raise -> rep -> b)
map op pipeline raise rep =
    pipeline raise rep |> op


init :
    ((rep -> interface) -> flags -> output)
    -> ((rep -> interface) -> rep -> interface)
    -> flags
    -> output
init initOp pipeline flags =
    let
        raise : rep -> interface
        raise rep =
            pipeline raise rep
    in
    initOp raise flags
  • multiple functions
  • :sparkles: magic :sparkles: happens in the init function
2 Likes

This topic will close but we want the recap ! :stuck_out_tongue:
This is wonderful to observe recursion as a tool to automate things like hiding the type.
In fact is this allowing us more than making common actions on different types (which is already huge) ?
If yes, what if we also want to do specific actions on those types, for example between intCounter and stringCounter specifically ? If we have the solution for both, would it be enough to make any system ?
I don’t expect answers to those questions in this thread as it is another subject, I’m just questioning about the next steps (because it is my current concern on my project).

Hey Arthur, what a great service! Thank you very, very much! :pray: I created a calendar entry which kept nagging me yesterday, but I thought I had created the reminder one day before the actual deadline :man_facepalming:

I fear I have no answer to your questions, but I’ll think about it.