Type Classes in Elm (sort of)

Hi there,

Here are two files: StringWriter.elm and CmdWriter.elm. (edit: links no longer work)

Both are implementations of the Writer monad. In particular, lets look at the two implementations of andThen:

andThen : (state1 -> StringWriter state2 ) -> StringWriter state1  -> StringWriter state2 
andThen fun ( a, out ) =
    let
        ( newA, newOut ) =
            fun a
    in
    ( newA
    , out |> (\c1 c2 -> c2 ++ c1) newOut
    )
andThen : (state1 -> CmdWriter state2 out) -> CmdWriter state1 out -> CmdWriter state2 out
andThen fun ( a, out ) =
    let
        ( newA, newOut ) =
            fun a
    in
    ( newA
    , out |> (\c1 c2 -> Cmd.batch [c1,c2]) newOut
    )

As you can see, we are using c2 ++ c1 for the StringWriter and Cmd.batch [c1,c2] for the CmdWriter. Additionally, CmdWriter has two type variables, whereas StringWriter has just one. Other than that, the two implementations are identical.

This is done by generating Elm files from templates using a little program

Here is the input that generated the two files:

{
  "generateInto": "elm-gen/generated",
  "templatesFrom": "elm-gen/templates",
  "moduleBase": "Gen",
  "modules": {
    "StringWriter": {
      "Writer": {
        "imports": [],
        "output": {
          "type": "String",
          "polymorphic": false,
          "empty": "\"\"",
          "append": "(\\c1 c2 -> c2 ++ c1)"
        }
      }
    },
    "CmdWriter": {
      "Writer": {
        "imports": [],
        "output": {
          "type": "Cmd",
          "polymorphic": true,
          "empty": "Cmd.none",
          "append": "(\\c1 c2 -> Cmd.batch [c1,c2])"
        }
      }
    }
  }
}

Isn’t this awesome? :star_struck:


This project is actually just a few hours old, but the things that can be done with this

I mean, the writer monad is just a showstopper. The real value behind this tool comes from the Enum and Record templates.

It will properly take a month or so until I can bring this tool into an alpha stage and officially announce it. But for now, I just wanted to share my excitement with you. :see_no_evil: :hear_no_evil: :speak_no_evil:

8 Likes

You are doing a very similar thing to what I describe in SimulatedHttp, functors and sed | Pole prediction dev blog though in a much more formal manner.
Basically I see this as more like O’caml style functors than type classes, but it is towards the same end.
I think the use-case I describe in the post (elm-program-test and HTTP) would be a good use-case for your developing tool.

1 Like

Yeah you are right, the idea is the same. Though the key difference is that your solution takes an existing module as an input and clones it.

My solution takes json as input and creates new files from an existing template (using Handlebars.js). So my programm can’t actually handle your problem.

1 Like

I’m not quite sure I understand why your tool couldn’t handle the problem I described? I think what you’re saying is that you couldn’t take an existing module and clone and then change the parts necessary. But that’s not necessary to solve the problem I was addressing.

I could just as easily use your approach. The only reason I didn’t use the ‘template’ approach is because the templates in my problem are quite complex and likely to change. That is, you’re going to be actively developing them, rather than your writer monads which are more of the kind “write once and they basically remain unchanged thereafter”. Because a templating language is “not-quite-elm” it means your editor plug-ins and possibly elm-format might not work. That’s a pain if you’re actively developing the template, but perfectly okay if you write it once and then can forget about it.

Having a template language is certainly “cleaner” and shows intent more explicitly. If it were successful enough, editor plug-ins and elm-format could be modified/augmented/branched to allow for such templates.

Im not quite sure with that. Yes, elm-format could be updated. But i bet, you can’t modify the elm-language-server to support templates, as they break any type system. So updating the template during development is something that i would not recommend.

1 Like

The project is now in Alpha.

You can find the repository here: Orasund/elm-pen and the documentation here.

1 Like

If type classes are something that gets you excited, can I ask why you are still using elm?

1 Like

Good question :rofl:

I really LOVE elm. So it’s not like I would move over to Haskell just for type classes.
Instead, it seems for me the obvious solution is to add the needed functionality to elm.

I still don’t understand why this is considered “needed functionality”. You can already achieve the desired effect by writing the generated elm files, and indeed the generated files are smaller than the templates that generate them!

You could ask the same question for polymorphic types (like List a).

Why do we need a map:(a->b) -> List a -> List b if we could simple write

  • mapIntToFloat:(Int->Float) -> List Int -> List Float
  • mapFloatToInt:(Float->Int) -> List Float -> List Int
  • mapFloatToFloat:(Float->Float) -> List Float -> List Float
  • mapIntToInt:(Int->Int) -> List Int -> List Int

The answer is quite simple: It is faster to write the implementation once and then just replace a and b with the needed types.


The same is with my Record template. I can write a function

map{{fieldName}}: ({{fieldType}} -> {{fieldType}}) -> {{record}} -> {{record}}
map{{fieldName}} fun record =
  { record | {{fieldName}} = fun record.{{fieldName}} }

and replace {{fieldName}}, {{fieldType}} and {{record}} with any combination of field and record. (like time, Posix and { time : Posix })


Hope this makes things clearer.

There is PureScript :wink:

1 Like

Yeah, and Scala and many other interesting languages. Some have Type Classes, some have other interesting ways of solving this problem.

But I am primarily interested in Elm.

I actually find your approach quite impressive and I really love to see how creative this community is when it comes to deal with “limitations” like this.

Due to some subjective dislike I’d rather not opt in into external code-generators (it took me a long while to give elm-spa a chance and I still have mixed feelings). Although I guess it’s a mild one here (as you probably would opt to check in the generated modules and not use the generator in CI etc.)

Who knows - maybe Evan likes this idea and someday it gets first-class support.

For the most parts you are right - I guess Elm does not really need type-classes - if only it would not have them semi-implemented (comparable and stuff).
As it is right now dictionaries etc. are really a pain if you like to use domain-value-objects.


BTW: It’s totally fine that you want to stick with Elm - it’s a great language. But in case you or somebody else who really want this type of ad-hoc polymorphism - do yourself a favour and at least take a look at PureScript - for me it sits in the golden middle between Haskell and Elm - it has “sane” records, a great FFI and many of the features people coming from Haskell tend to miss in Elm (like type-classes, higher-kinded types)
Of course it has it’s trade-offs and is arguably worse than Elm (generated size, performance, community-size, errors) - but It’s worth a look IMHO

2 Likes

Thanks for the reply.

What I’m hung up on is that this can be written in plain elm like this:

andThen : (out -> out -> out) -> (state1 -> (state2, out)) -> (state1, out) -> (state2, out)
andThen append fun (a, out) =
    let
        ( newA, newOut ) =
            fun a
    in
    ( newA
    , out |> append newOut
    )


stringWriterAndThen = andThen (\c1 c2 -> c2 ++ c1)
cmdWriterAndThen = andThen (\c1 c2 -> Cmd.batch [c1, c2])

What value is the code generator adding?

stringWriterAndThen = andThen (\c1 c2 -> c2 ++ c1)
cmdWriterAndThen = andThen (\c1 c2 -> Cmd.batch [c1, c2])

That’s exactly the point. You have to manually add new functions just to provide the append method.

You could of course add the append method to your type

type alias Appendable a =
  { content = a
  | append = a -> a -> a
  }

But the same trick does not work for the andThen function or the map function:

type alias Monad t =
  { content = t
  , andThen : (a -> b) -> Monad a -> Monad b
  , empty : t
  }

a and b need to be annotated as well, but that just turns into a loop (you now need to add 4 new variables to the andThen function, these need to be annotated again, so add 8 more variables to
 you get the point).


So in conclusion, we can’t write a monad. But with Elm-Pen you now can. You can just provide all the function where you need andThen and the generator can replace it for you.

Is this different than what you’re trying to accomplish?

type alias Writer out emptyA andThenA andThenB =
    { empty : emptyA -> ( emptyA, out )
    , andThen : (andThenA -> ( andThenB, out )) -> ( andThenA, out ) -> ( andThenB, out )
    }


writer : out -> (out -> out -> out) -> Writer out a b c
writer empty append =
    { andThen = andThen append
    , empty = \contents -> ( contents, empty )
    }


andThen : (out -> out -> out) -> (state1 -> ( state2, out )) -> ( state1, out ) -> ( state2, out )
andThen append fun ( a, out ) =
    let
        ( newA, newOut ) =
            fun a
    in
    ( newA
    , out |> append newOut
    )


stringWriter =
    writer "" (\c1 c2 -> c2 ++ c1)


cmdWriter =
    writer Cmd.none (\c1 c2 -> Cmd.batch [ c1, c2 ])


{-| example
-}
output =
    stringWriter.empty 1
        |> stringWriter.andThen (\a -> ( a + 1, String.fromInt a ))
        |> stringWriter.andThen (\a -> ( a + 1, String.fromInt a ))
        |> Tuple.second

Main usage of type classes comes from HKT. Then you can write your function that accepts any Writer and uses it’s functions but is not concerned with type inside Writer. Code generation can you get so far. If we ignore this use case, then “type classes” can be implemented with extensible records where some fields are just fields with closed over record.

2 Likes

First, thanks for your answer. It really got me thinking.

Second, your code works because you are specifying springWriter explicitly.

Your code no longer complies if you try to abstract away to an arbitary writer:

run w =
    w.empty 1
        |> Tuple.second
        |> Html.text

{-| example
-}
main =
    let
        _ = run cmdWriter
    in
    run stringWriter

So yes. You are right. For this explicit example, my solution would not be very elegant. And yes, I am actually very impressed how far one can come if one just looks at the functions separate of the type (I always tried to put both into the same record).


I still see the need for my tool, though. Maybe not for specifying a writer - and I might be removing this example in the final version of the tool - but for Enums, Records or any example where you actually generate a function with some generated logic in it.

This doesn’t compile because Html.text requires a string. This does compile:

run w =
    w.empty 1
        |> Tuple.second

main =
    let
        _ = run cmdWriter
    in
    run stringWriter
        |> Html.text

I was thinking more about what @solificati said, and I think the biggest limitation to my approach is that you need to pass the monad operations multiple times if you want to be able to operate on monads containing different types. For example, you need[1] to write:

doSomething writerOperations1 writerOperations2 writerOperations3 mapIntOut mapFloatOut =
    writerOperations1.empty 3
        |> writerOperations2.andThen (\a -> ( toFloat a, mapIntOut a ))
        |> writerOperations3.andThen (\a -> ( String.fromFloat a, mapFloatOut a ))
        |> Tuple.second

when it feels like you should be able to write:

doSomething writerOperations mapIntOut mapFloatOut =
    writerOperations.empty 3
        |> writerOperations.andThen (\a -> ( toFloat a, mapIntOut a ))
        |> writerOperations.andThen (\a -> ( String.fromFloat a, mapFloatOut a ))
        |> Tuple.second

  1. I passed 3 values for the sake of example, but only two are strictly necessary here since the empty operation binds different type variables than the andThen operation. ↩

2 Likes

+1 to this. I got my FP start in Elm, but when I decided to give PureScript a try, I ended up never looking back. I didn’t implement type classes like you @Lucas_Payr (frankly I didn’t even know about them at the time), but I did implement an FFI that was quite useful (and still works if anyone’s curious). It was really nice to move to a language that had these things already built in. Also fun fact that I realized relatively early on about PureScript that really solidified it for me: you can essentially build Elm within PureScript! Okay, plug over, bottom line, if you wanna go beyond what Elm gives you, give PureScript a try!

1 Like