Discussion:
[elm-discuss] elm-css with type classes (a response to the Elm Town podcast)
Mitchell Rosen
2017-04-27 02:08:37 UTC
Permalink
Hello all! I am a (very) beginner Elm programmer, but I know a fair bit of
Haskell. I listened to the most recent Elm Town podcast and was especially
intrigued by the discussion around a novel use of record types to solve a
real-world problem - embedding a nice CSS DSL directly into Elm. Awesome!

I was further intrigued by Evan's suggestion that type classes were in fact
a poor solution to this problem. I'm well aware that type classes are an
overused abstraction in Haskell that can cause lots of pain in unintended
ways, especially when using them for the sole purpose of reusing nice,
short function names.

This is exactly what a CSS DSL must do, however - we'd like to be able to
say both *"overflow: hidden" *and "border-style: hidden" in our DSL by
reaching for the same intuitive name *hidden* and just have everything
magically work. Sounds like exactly the difficult sort of modeling problem
that type classes seem *superficially* suitable, but are often not in
practice.

However, after looking up the venerable elm-css
<https://github.com/rtfeldman/elm-css> library and peeking around its
internals, I'm not sure I agree. The implementation is, in a word,
abstruse. The types that end-users have to see and interact with are highly
non-intuitive. There's a lot of data being slogged around at runtime to
satisfy the type checker. It's simply not a good abstraction. The DSL that
came out on the other end looks great, but at what cost?

For some context, here's how I'd proceed if I had to implement this in
Haskell with type classes.

Say we want to model the key *margin* which can have a value of *auto* or
some integer (simplifying a bit here). I'd write the following type class:

* class Margin a where*
* margin :: a -> Style*

Then, I'd write two instances for the type class - one for my made-up type
*auto* and another for *Int*.

* data Auto = Auto*

* instance Margin Auto where*



* margin Auto = {- implementation... -} instance Margin Int where
margin n = {- implementation... -}*

Here's what the final type of the *margin* combinator looks like:

* margin :: Margin a => a -> Style*

and I could use it like so:

* margin Auto*
* margin 5*

Now, I'm not necessarily advocating this approach (DSL design is hard), but
there it is, for reference. I happen to share a lot of the sentiments
expressed in the podcast about type classes, but in this specific case,
they actually don't seem too bad. Compare the above with the machinery in
for margin that exists in elm-css:

*type alias LengthOrAuto compatible =*


* { compatible | value : String, lengthOrAuto : Compatible }*
* margin : LengthOrAuto compatible -> Style*

* auto :*
* { lengthOrAuto : Compatible*
* , overflow : Compatible*
* , textRendering : Compatible*
* , flexBasis : Compatible*
* , lengthOrNumberOrAutoOrNoneOrContent : Compatible*
* , alignItemsOrAuto : Compatible*
* , justifyContentOrAuto : Compatible*
* , cursor : Compatible*
* , value : String*
* , lengthOrAutoOrCoverOrContain : Compatible*
* , intOrAuto : Compatible*
* }
*

Compatible, what? Some value field is a String? Keep in mind these type are
all visible to the user - necessarily so - so they can figure out how to
fit the pieces together. However, I don't think it's unreasonable to say
that the only way (or at least the *main *way) to use this library is to
shut your brain off and trust that if you get any compiler error, it's
because you did some illegal CSS thing. It's simultaneously exposes ugly
innards (type aliases) and yet resists when you try to grok what's going on
(Compatible is not exported, though for good reason).

It concede it's possible I have overlooked some part of the library where
the simple type class approach breaks down. But when attacked top-down (I
want the DSL to come out looking like *this *so I need to use *these
language features*), I'm not seeing why one would prefer this row-types
approach over type classes if both happened to exist in Elm.

Thanks for reading, and if this is the umpteenth post about type classes, I
apologize. I figured at least the concept of type classes still relevant in
this community, given they were briefly discussed on Elm Town just today.
I'd love to hear any seasoned Elm-ers thoughts about row types as an
abstraction mechanism, type classes or lack thereof, elm-css, or anything
else that came to mind while reading over this post.

Mitchell
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Joey Eremondi
2017-04-27 06:35:13 UTC
Permalink
I might be misunderstanding, but in your use case, you probably want a
tagged Union, what Haskell calls data, instead of type classes. Basically,
they're always preferable when you have a fixed number is variants you
want. Your margin function would then just pattern match.

type Margin = Auto | IntMargin Int

margin: Margin -> Style
margin m = case m of ...

Is there some visual overhead having to have a tag for Int? Yes. Is your
code clearer and simpler? Yes.

Haskell classes are made for abstractions with many instances defined
separately. Things like traversable, Monads, Monoid, etc. There are many
things fitting the pattern, and we want to give people the ability to
implement it for their own types. Most experienced Haskell coders would not
use a type class in your use case.

Classes also have problems if you want to make a list of margins for some
reason. Now you can't without existential types, which are a whole
different kettle of fish. Tagged unions don't have that problem.

On Apr 26, 2017 8:03 PM, "Mitchell Rosen" <***@gmail.com> wrote:

Hello all! I am a (very) beginner Elm programmer, but I know a fair bit of
Haskell. I listened to the most recent Elm Town podcast and was especially
intrigued by the discussion around a novel use of record types to solve a
real-world problem - embedding a nice CSS DSL directly into Elm. Awesome!

I was further intrigued by Evan's suggestion that type classes were in fact
a poor solution to this problem. I'm well aware that type classes are an
overused abstraction in Haskell that can cause lots of pain in unintended
ways, especially when using them for the sole purpose of reusing nice,
short function names.

This is exactly what a CSS DSL must do, however - we'd like to be able to
say both *"overflow: hidden" *and "border-style: hidden" in our DSL by
reaching for the same intuitive name *hidden* and just have everything
magically work. Sounds like exactly the difficult sort of modeling problem
that type classes seem *superficially* suitable, but are often not in
practice.

However, after looking up the venerable elm-css
<https://github.com/rtfeldman/elm-css> library and peeking around its
internals, I'm not sure I agree. The implementation is, in a word,
abstruse. The types that end-users have to see and interact with are highly
non-intuitive. There's a lot of data being slogged around at runtime to
satisfy the type checker. It's simply not a good abstraction. The DSL that
came out on the other end looks great, but at what cost?

For some context, here's how I'd proceed if I had to implement this in
Haskell with type classes.

Say we want to model the key *margin* which can have a value of *auto* or
some integer (simplifying a bit here). I'd write the following type class:

* class Margin a where*
* margin :: a -> Style*

Then, I'd write two instances for the type class - one for my made-up type
*auto* and another for *Int*.

* data Auto = Auto*

* instance Margin Auto where*



* margin Auto = {- implementation... -} instance Margin Int where
margin n = {- implementation... -}*

Here's what the final type of the *margin* combinator looks like:

* margin :: Margin a => a -> Style*

and I could use it like so:

* margin Auto*
* margin 5*

Now, I'm not necessarily advocating this approach (DSL design is hard), but
there it is, for reference. I happen to share a lot of the sentiments
expressed in the podcast about type classes, but in this specific case,
they actually don't seem too bad. Compare the above with the machinery in
for margin that exists in elm-css:

*type alias LengthOrAuto compatible =*


* { compatible | value : String, lengthOrAuto : Compatible }*
* margin : LengthOrAuto compatible -> Style*

* auto :*
* { lengthOrAuto : Compatible*
* , overflow : Compatible*
* , textRendering : Compatible*
* , flexBasis : Compatible*
* , lengthOrNumberOrAutoOrNoneOrContent : Compatible*
* , alignItemsOrAuto : Compatible*
* , justifyContentOrAuto : Compatible*
* , cursor : Compatible*
* , value : String*
* , lengthOrAutoOrCoverOrContain : Compatible*
* , intOrAuto : Compatible*
* }
*

Compatible, what? Some value field is a String? Keep in mind these type are
all visible to the user - necessarily so - so they can figure out how to
fit the pieces together. However, I don't think it's unreasonable to say
that the only way (or at least the *main *way) to use this library is to
shut your brain off and trust that if you get any compiler error, it's
because you did some illegal CSS thing. It's simultaneously exposes ugly
innards (type aliases) and yet resists when you try to grok what's going on
(Compatible is not exported, though for good reason).

It concede it's possible I have overlooked some part of the library where
the simple type class approach breaks down. But when attacked top-down (I
want the DSL to come out looking like *this *so I need to use *these
language features*), I'm not seeing why one would prefer this row-types
approach over type classes if both happened to exist in Elm.

Thanks for reading, and if this is the umpteenth post about type classes, I
apologize. I figured at least the concept of type classes still relevant in
this community, given they were briefly discussed on Elm Town just today.
I'd love to hear any seasoned Elm-ers thoughts about row types as an
abstraction mechanism, type classes or lack thereof, elm-css, or anything
else that came to mind while reading over this post.

Mitchell
--
You received this message because you are subscribed to the Google Groups
"Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an
email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Mitchell Rosen
2017-04-27 12:21:21 UTC
Permalink
Hi Joey,

Indeed, basic ADTs are one way to model the DSL. The problem then arises when you want to reuse the name "auto" for another key.

I may not have been clear, so I'll try to summarize my post here. How might elm-css look if there were different features in Elm? As it stands, the library seems to be fighting hard against the language, and the result far from beginner-friendly.

Type classes are one such extension, and I sketched out how one might implement this library using them. As you mentioned, ADTs are another.

In this particular case, type classes, as flawed as they are, seem vastly superior to the row-types approach. But, I'm definitely open to having my mind changed! I'm just not comfortable enough with row types as a language feature to really have an intuitive understanding of when to use them, and when to not.

But, anyways, I totally agree that keeping it simple with ADTs is what you should do most of the time. It's only when you're writing library code to be used by the community that you should start obsessing over the ergonomics.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
OvermindDL1
2017-04-27 17:53:45 UTC
Permalink
Actually this sounds to exactly like the use-case for OCaml's Polymorphic
Variants. In OCaml you could easily use just `Hidden for both of your
examples, fully type checked, type safe, etc... etc... Polymorphic
Variants are just global Variants that are unnamed. A function can take a
bounded or unbounded set of them and are perfectly suited for an abstract
CSS DSL while being readable in both usage and implementation.

And yes, I agree about type classes, they are extremely over-used in
Haskell where something like OCaml's Implicit Modules would be such a
significantly better fit for...
Post by Mitchell Rosen
Hi Joey,
Indeed, basic ADTs are one way to model the DSL. The problem then arises
when you want to reuse the name "auto" for another key.
I may not have been clear, so I'll try to summarize my post here. How
might elm-css look if there were different features in Elm? As it stands,
the library seems to be fighting hard against the language, and the result
far from beginner-friendly.
Type classes are one such extension, and I sketched out how one might
implement this library using them. As you mentioned, ADTs are another.
In this particular case, type classes, as flawed as they are, seem vastly
superior to the row-types approach. But, I'm definitely open to having my
mind changed! I'm just not comfortable enough with row types as a language
feature to really have an intuitive understanding of when to use them, and
when to not.
But, anyways, I totally agree that keeping it simple with ADTs is what you
should do most of the time. It's only when you're writing library code to
be used by the community that you should start obsessing over the
ergonomics.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Mark Hamburg
2017-04-27 20:26:04 UTC
Permalink
Maybe this is covered in OCaml's polymorpic variants — I couldn't tell from
a quick skim — but another feature that would cover this without
introducing type classes is support for more arbitrary union/sum types.
That way, one could write something like:

type Auto = Auto


type alias LengthOrAuto = Int + Auto -- is this a type or a type alias, I'm
not sure

margin : LengthOrAuto -> Style

Handwaving type checking rules would require that a type passed to
something that was expecting a union type needs to cover a subset of that
union type. Case statements would be used to do the discrimination.

The potentially interesting things that come out of something like this are
that one could imagine a union of Maybe and Result which would allow for a
value or an error or nothing. This has got to have been explored somewhere
before. I have no idea what sort of problems it might create for the type
system.

Mark
Post by OvermindDL1
Actually this sounds to exactly like the use-case for OCaml's Polymorphic
Variants. In OCaml you could easily use just `Hidden for both of your
examples, fully type checked, type safe, etc... etc... Polymorphic
Variants are just global Variants that are unnamed. A function can take a
bounded or unbounded set of them and are perfectly suited for an abstract
CSS DSL while being readable in both usage and implementation.
And yes, I agree about type classes, they are extremely over-used in
Haskell where something like OCaml's Implicit Modules would be such a
significantly better fit for...
Post by Mitchell Rosen
Hi Joey,
Indeed, basic ADTs are one way to model the DSL. The problem then arises
when you want to reuse the name "auto" for another key.
I may not have been clear, so I'll try to summarize my post here. How
might elm-css look if there were different features in Elm? As it stands,
the library seems to be fighting hard against the language, and the result
far from beginner-friendly.
Type classes are one such extension, and I sketched out how one might
implement this library using them. As you mentioned, ADTs are another.
In this particular case, type classes, as flawed as they are, seem vastly
superior to the row-types approach. But, I'm definitely open to having my
mind changed! I'm just not comfortable enough with row types as a language
feature to really have an intuitive understanding of when to use them, and
when to not.
But, anyways, I totally agree that keeping it simple with ADTs is what
you should do most of the time. It's only when you're writing library code
to be used by the community that you should start obsessing over the
ergonomics.
--
You received this message because you are subscribed to the Google Groups
"Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an
For more options, visit https://groups.google.com/d/optout.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
OvermindDL1
2017-04-27 21:08:28 UTC
Permalink
Yep, that is all trivial with Polymorphic Variants if you want them shared.

In essence, you know how a normal variant is, like this (in OCaml syntax
since that is what I'm more comfortable with, but it's close enough to Elm,
'almost' identical)
```ocaml
type Vwoop =
| Infinite
| Int of integer

type Broop =
| Infinite
| Float of float
```
And say you have some functions like (again in OCaml syntax):
```ocaml
let vloo = function
| Infinite -> "infinite"
| Int i -> string_of_integer i

let bloo = function
| Infinite -> "all"
| Float f -> string_of_float f
```
Now this will not compile, there are two Infinite's in scope and so it does
not know which you want or mean without qualifying them by name. If
however we did not have the variant types above and just used polymorphic
variants:
```ocaml
let vloo = function
| `Infinite -> "infinite"
| `Int i -> string_of_int i

let bloo = function
| `Infinite -> "all"
| `Float f -> string_of_float f
```
And this all works fine, you could use it like this no problem:
```ocaml
let whatever = String.concat [vloo `Infinite; bloo `Infinite]
```
And `whatever` will become the string "infiniteall" as expected.

A Polymorphic Variant is just a 'globally scoped' variant, and it is not
based on the name but the entire type, so ``` `Int ``` is different from
``` `Int 42 ```, so this will *not* compile:
```ocaml
let this_does_not_compile = vloo (`Int 42.7)
```
This will give you the error (I just ran this to test):
```
Line 9, 33: Error: This expression has type [> `Int of float ]
but an expression was expected of type [< `Infinite | `Int of int ]
Types for tag `Int are incompatible
```
So it is trying to pass in the specific type of `Int of float, yet the
function only accepts the closed set of polymorphic varianst of `Infinite
or `Int of int. It then goes on to clarify that the `Int is fine but the
type inside the `Int is wrong/incompatible. For further example if I try
to past in `Blah it returns:
```
Line 9, 33: Error: This expression has type [> `Blah ]
but an expression was expected of type [< `Infinite | `Int of int ]
The second variant type does not allow tag(s) `Blah
```
It is type safe the whole way through. And of course you can add types to
it as well if you want, they are exactly as they appear in the error
messages.


For note, `function` is a keyword in OCaml that is short for a combination
fn and match, so these are the same:
```ocaml
let tester = function
| `Test1 -> "test1"
| `Test2 -> "test2"

let tester t = match t with
| `Test1 -> "test1"
| `Test2 -> "test2"
```

For note, polymorphic variants are just like normal variants, in the
generated javascript they get tagged like any normal variant, just with a
larger number due to their scope. The above tester/bloo/vloo functions
compile to this javascript:
```javascript
var Pervasives = require("stdlib/pervasives");

function vloo(param) {
if (typeof param === "number") {
return "infinite";
}
else {
return Pervasives.string_of_int(param[1]);
}
}

function bloo(param) {
if (typeof param === "number") {
return "all";
}
else {
return Pervasives.string_of_float(param[1]);
}
}

function tester(t) {
if (t >= 549646208) {
return "test2";
}
else {
return "test1";
}
}
```

But yes, as you can see polymorphic variants types can be open (accepting
any) or closed (accepting a limited subset), fully typed and safe the whole
way down.

A lot of people consider polymorphic variants existence a bit of a wart,
but I do not see it, they are highly highly useful in specific cases (like
DSEL's here), but I do agree they can be abused when normal variants are
better...
Post by Mark Hamburg
Maybe this is covered in OCaml's polymorpic variants — I couldn't tell
from a quick skim — but another feature that would cover this without
introducing type classes is support for more arbitrary union/sum types.
type Auto = Auto
type alias LengthOrAuto = Int + Auto -- is this a type or a type alias,
I'm not sure
margin : LengthOrAuto -> Style
Handwaving type checking rules would require that a type passed to
something that was expecting a union type needs to cover a subset of that
union type. Case statements would be used to do the discrimination.
The potentially interesting things that come out of something like this
are that one could imagine a union of Maybe and Result which would allow
for a value or an error or nothing. This has got to have been explored
somewhere before. I have no idea what sort of problems it might create for
the type system.
Mark
Post by OvermindDL1
Actually this sounds to exactly like the use-case for OCaml's Polymorphic
Variants. In OCaml you could easily use just `Hidden for both of your
examples, fully type checked, type safe, etc... etc... Polymorphic
Variants are just global Variants that are unnamed. A function can take a
bounded or unbounded set of them and are perfectly suited for an abstract
CSS DSL while being readable in both usage and implementation.
And yes, I agree about type classes, they are extremely over-used in
Haskell where something like OCaml's Implicit Modules would be such a
significantly better fit for...
Post by Mitchell Rosen
Hi Joey,
Indeed, basic ADTs are one way to model the DSL. The problem then arises
when you want to reuse the name "auto" for another key.
I may not have been clear, so I'll try to summarize my post here. How
might elm-css look if there were different features in Elm? As it stands,
the library seems to be fighting hard against the language, and the result
far from beginner-friendly.
Type classes are one such extension, and I sketched out how one might
implement this library using them. As you mentioned, ADTs are another.
In this particular case, type classes, as flawed as they are, seem
vastly superior to the row-types approach. But, I'm definitely open to
having my mind changed! I'm just not comfortable enough with row types as a
language feature to really have an intuitive understanding of when to use
them, and when to not.
But, anyways, I totally agree that keeping it simple with ADTs is what
you should do most of the time. It's only when you're writing library code
to be used by the community that you should start obsessing over the
ergonomics.
--
You received this message because you are subscribed to the Google Groups
"Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an
For more options, visit https://groups.google.com/d/optout.
--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss+***@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.
Loading...