Polymorphic functions in Elm

922 views
Skip to first unread message

Paul Blair

unread,
Oct 1, 2015, 10:44:02 AM10/1/15
to Elm Discuss
As a way of getting up to speed in Elm, I've been working on making a port of a Haskell library. For typeclasses I decided to take the approach of using records of functions. However, I've run into problems with recursion -- maybe I haven't understood this approach correctly:

For example:

type Value = Value String

type alias ValueWrapping a = { _value : a -> Value }

-- The polymorphic function
value : ValueWrapping a --> a > Value
value wrapping obj = wrapping._value obj

-- And now the actual records of functions

stringValueWrapping : ValueWrapping String
stringValueWrapping = { _value = \x -> Value x }

intValueWrapping : ValueWrapping Integer
intValueWrapping = { _value = \x -> Value (toString x) }

-- And now the recursive problem:

maybeValueWrapping : ValueWrapping Maybe a
intValueWrapping = 
  { _value = \x -> 
        case x of 
           Just a -> value ??? a           -- The problem. 
           Nothing -> Value ""
  }

Since Maybe is parametrically polymorphic, there doesn't seem to be a way for me to choose the right record to accompany the value wrapped in the Maybe, without already having the polymorphism I'm trying to emulate. Is there some clever way around this? From what I can see, the standard library handles a polymorphic function like toString by calling out to the underlying native implementation.

If there isn't a way around this, is there a way of taking the value function with a generic parameter a and pattern matching on the type of the parameter? That'll at least get me polymorphism, even if it's not ad-hoc.

Joey Eremondi

unread,
Oct 1, 2015, 11:41:59 AM10/1/15
to elm-d...@googlegroups.com
If you look at the Haskell typeclass, you would see something like this:
instance ValueWrapping a => ValueWrapping (Maybe a)

In other words, there's an extra constraint on the type variable a.

In Elm, you have to do the same thing, you just turn all the => into -> and pass things as parameters.

maybeValueWrapping : ValueWrapping a -> ValueWrapping (Maybe a)
intValueWrapping subInstance = 
  { _value = \x -> 
        case x of 
           Just a -> subInstance._value a 
           Nothing -> Value ""
  }


--
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.

Paul Blair

unread,
Oct 1, 2015, 4:41:31 PM10/1/15
to Elm Discuss
I just found this thread:


which seems already to have hashed this issue to death -- sorry for duplicating the discussion. From that thread I conclude that there's currently no way to do what I'm trying to do.

I'm glad that the community is so cautious about adding additional features to the language. One of the things I'm liking very much about Elm is the selectivity and judiciousness with which features have been selected for the language. Having been poking around at Haskell lately, and worked in Scala before that, Elm feels like a great load off my mind.

That said, I wonder if allowing pattern matching based on type would introduce much complexity to the language. F#, for example, lets you do this:

let RegisterControl control =
    match control with
    | :? Button as button -> button.Text <- "Registered."
    | :? CheckBox as checkbox -> checkbox.Text <- "Registered."
    | _ -> ()

This would at least allow for some degree of polymorphism, though since the match alternatives are closed, new types couldn't be added on an ad-hoc basis.

For ad-hoc polymorphism it sounds like the discussion so far has covered type classes as well as implicit parameters. Having worked with Scala, I'm not a big fan of the implicit parameter approach. This kind of magic makes code extremely hard to follow and reason about, and violates the principle of locality. You're always trying to figure out in which scope you should bind your implicit parameter and whether you might be pulling in a parameter defined in some other scope instead. It's really not fun.

There's a lot of literature out there on ad-hoc polymorphism and the expression problem; type classes and implicit parameters aren't necessarily the only alternatives. One interesting approach, particularly for Elm with its extensible records, would be having first-class case expressions as proposed by this paper: http://people.cs.uchicago.edu/~blume/papers/icfp06.pdf ("Extensible Programming with First-Class Cases"). The paper is extremely technical and mostly beyond me, but here is the money quote:

"The cases of a match expression handling a sum type r and returning a value of type t are represented by first-class values of type r ,→ t , and these
values are extensible in the same sense in which records can be extended with new fields." (Note that "One can think of ,→ as an alternative function arrow whose elimination form [is the match construct].")

What I take from this is that a function that is basically a single case expression that takes an extensible record as a parameter, could be composed with a function which would add additional cases for additional extensions of that record type. It's an interesting idea; not sure how well it would work with Elm's record semantics (or whether the extra cognitive overhead would make it worthwhile to implement).

Joey Eremondi

unread,
Oct 1, 2015, 4:47:38 PM10/1/15
to elm-d...@googlegroups.com

Can you talk more about the specific problem you are trying to solve? I'm not convinced that what you're trying to do in Elm. Other than verbosity, what can't explicit parameters solve?

Whatever gets integrated, it will definitely be based on clear use cases and examples.

Paul Blair

unread,
Oct 2, 2015, 11:11:01 AM10/2/15
to Elm Discuss
The high-level of what I'm trying to do is see if I can bring the Clay CSS library over to Elm from Haskell. It's a pretty comprehensive typed CSS DSL, with a not-impossibly-big codebase, and I thought it would be an interesting way to learn Elm (and improve my Haskell, which is still pretty rudimentary).

Clay uses a Writer monad called Css to aggregate Css rules. That part is easy: I can just use a data type that contains a list of rules, and pass it as a parameter. I should be able to use partial function application and function composition to get something that isn't much more verbose than do-syntax.

A Css rule is a recursive data type, one of whose alternatives is a Property. A Property is formed of Key and Value, where Value can wrap any number of things that could be the value of a CSS property: String, Literal, Integer, Float, Maybe, tuple, Either, or list. The last four of these are parameterized types and could contain other values. E.g., one could have an Either (List String) (Maybe Literal). 

The value function takes one of these types and wraps it in a Value. Theoretically, I could have a whole family of these value functions: wrapString, wrapLiteral, wrapInteger... etc. But I still get into problems with the recursive types. If I want to do wrapMaybe, the value that the Maybe contains also has to be wrapped. 

In Haskell, it looks like this:

class Val a where
 
value :: a -> Value


instance Val Integer where
 
value = fromString . show

...


instance Val a => Val (Maybe a) where

  value (Just a) value a

  value Nothing  = ""


Any suggestions for how to do this in Elm?

Joey Eremondi

unread,
Oct 2, 2015, 12:46:32 PM10/2/15
to elm-d...@googlegroups.com
 I think the problem is, you're trying to define a function "value" which works in all your cases. This won't work. You just use the record field wherever you would use "value" in Haskell.


Again, I apologize if I'm missing the obvious, but I think this should work.

This is nice for a few reasons:
1. You're making it explicit that ad-hoc polymorphism is happening here, as opposed to Haskell, where you can't tell if a function is normal or overloaded just by looking at it. Somone new to your code doesn't wonder "where does this value function come from?"
2. There's no problems with overlapping instances, orphan instances, etc.

Yes, it's more verbose, and you keep passing your instance arguments down and building them up, but it's more in the spirit of functional programming: things aren't left implicit.

Max Goldstein

unread,
Oct 2, 2015, 1:42:53 PM10/2/15
to Elm Discuss
You are correct that Elm 0.15 cannot define a function that takes many different types and turns them all into a single type. If you could, applying that function in a Maybe would be doable, and indeed, you can do with toString : a -> String (but again, you cannot write such a function yourself).

toStringMaybe : Maybe a -> String
toStringMaybe m =
  case m of
     Just x -> toString x
     Nothing -> ""

As you know, union types (ADTs) can be used to wrap many possible types as a single type. This is how Json.Encode works: each tag is wrapped with a function (for SemVar reasons), but basically, you just have to know which encoder you need for the type you have. You'll notice that for arrays, you need to map over some encoder to get a list or array of values, before you encode the array itself. And that's how it would work for Maybes: use Maybe.map to get a Maybe Val, and then Maybe.withDefault to provide the value for the Nothing case.

Hopefully that was helpful and told you something you didn't already know. Also, we managed to do all of this without records.

Paul Blair

unread,
Oct 2, 2015, 2:43:13 PM10/2/15
to Elm Discuss
Thanks to both of you -- I see it now. The things I didn't see originally were:

1. In my own code, I should have seen that e.g., the maybeValueWrapping had to take a Maybe (ValueWrapping a) and not just a Maybe a. I was focused on getting the whole Maybe wrapped "in one shot," but  neglecting the fact that even in Haskell the type parameter has to be constrained to a ValueWrapping or the operation doesn't make sense.

2. Joey, thanks for coding it out for me -- I now see that the wrapping functions for the composite types are functions of ValueWrapper a -> ValueWrapper (Composite a) and not just simple functions that produce a ValueWrapper from a composite type. That allows for composing the records of functions recursively, which I didn't see how to do.

3. I was trying to avoid "just having to know which encoder you need for the type you have," as Max puts it, because that means that at each point where I need to wrap a type, I'll have to cook up an ad-hoc pipeline to compose the encoders for that specific type. It would be nice to eliminate this boilerplate, but it's not impossible to work with. And I agree that there are lots of advantages to keeping everything explicit. 

Thanks for the help--really liking this language!

--
You received this message because you are subscribed to a topic in the Google Groups "Elm Discuss" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/elm-discuss/y61Bu0HDmIs/unsubscribe.
To unsubscribe from this group and all its topics, send an email to elm-discuss...@googlegroups.com.

Evan

unread,
Oct 5, 2015, 4:15:16 PM10/5/15
to Elm Discuss, cir...@phobot.net
I'd also check out libraries like Random and Json.Decode for examples of cases where Haskell would have done something with type classes, and (in my opinion) the Elm version is actually way easier to understand and use because it did not. Not sure if the pattern used in those examples will work for you, but I imagine it'd be good to look around at examples of a more "Elm way" of doing this sort of stuff.
Reply all
Reply to author
Forward
0 new messages