Wondering about Typeclasses

Because how we write and structure our code is one of the ways we can communicate our intent. An implementation that explicitly uses Maybe.map instead of Functor.map tells you something different about that function.

Both approaches have their uses, they aren’t dichotomous.

2 Likes

Some part of me really appreciates the idea of extracting the abstract pattern of the monad, and perhaps there’s a cool way to keep it simple, and still do that.

I suspect this might be an interesting direction for Elm some day :blush:

1 Like

Lack of this type of polymorphism is actually one of my favorite things about Elm. It means I only have to reason about what the code actually does and not what it could do if provided with different types.

I’ve only worked with polymorphism in the OO world, and not with Typeclasses, though, so it could be that it’s better to work with in that paradigm.

I’m not completely sure what you’re referring to. If you have some function working on Maybes using Maybe.map, as long as your type signature restricts the input to only Maybes, fmap will work the exact same way—in fact it will be solved to become Maybe.map (assuming instance Functor Maybe has fmap = Maybe.map).

Could you give an example where you need to reason about different types? From my experience, that’s exactly what typeclasses avoid, by exposing general operations that work regardless of the specific type.

I think there is an implicit assumption that polymorphism is desirable. This is fairly clearly the case in Haskell, where functions are often built with a need-to-know basis about the types they operate on. Typeclasses are a piece of that story as they enable interface oriented programming.

Generally Elm has taken a much more reserved approach to polymorphic programming. Generally it’s either parametric polymorphism (i.e. you need to know nothing about a value apart from its mere existence) or nothing. Extensible records and built in special variables (i.e. number) provide some small compromise on these. Personally I’m not sure if Elm was designed from scratch again that the special type variables would have made the cut.

This leads to Elm generally having a very different style of programming than Haskell despite the superficial syntactic similarities. Elm code forces you to focus on data structures, since you are in sense committing to them.

RE HKT:

What type would you give fmap? It can’t be fmap : (Functor c, Functor d) => (a -> b) -> c -> d since that would prevent type checking further down the line. But fmap : Functor f => (a -> b) -> f a -> f b

For the constraint, the kind doesn’t matter as it’s just checking that the type (without any parameters) is an instance of the typeclass. For the types in the function signature, they are all of kind * after being given their type parameters because I’m pretty sure a function can’t return or take an HKT.

I think the answer lies mostly in this blog post and especially the “Survivorship Bias” section.

When you are a beginner you see fmap in Haskell, you need to learn:

  • what a typeclass is
  • what a functor is
  • how to implement one for your own data type

In Elm, when you see List.map, that’s it.

It’s a design decision of the language, to prefer being accessible to beginners rather than flexible to experts.

2 Likes

Not really. It checks if type with specific numbers of parameters (missing) is an instance of a typeclass. Either a is instance of functor typeclass (one type parameter missing) and Either is an instance of bifunctor typeclass (two type parameter missing). This number of missing parameters is needed ( and its called a kind)

By constraint, I meant when you do something like Functor f. For the instance declaration for Either being a Functor, it makes sense that you need to partially apply the type. Now I understand. That is definitely necessary, but as a side note I’m curious how many typeclasses end up being partially applied like that. I’ve never done it and honestly it just seems confusing to make Either a Functor considering it can have two values… and what if you wanted fmap to act on the left value? Seems unnecessary to me.

Either is a functor. fmap operates on Right values.

To be precise, Either a is a functor. :slight_smile:

1 Like

Are you wondering about real world use cases? Validation is most common one. You map value until something breaks and you short circuit with error. It is very common case.

From theory standpoint? Because you can :slight_smile:

You always want to code to the smallest contact. And often you are interested only in one value inside Either. If you’re interested in both you probably need Bifunctor.

Think about it as function application and currying. We can partially apply function from left just like fmap a functor. But you can flip function and you can flip your type and provide functor instance. Or you can use mapLeft from Data.Either.Combinators

Just yesterday I had to decode some non-trivial Json and I stumbled upon elm-json-decode-pipeline. I had a very hard time understanding how required works, and in the end it looks like an hack to me.

type alias User =
  { id : Int
  , email : String
  }


userDecoder : Decoder User
userDecoder =
  Decode.succeed User
    |> required "id" int
    |> required "email" string)

How do you explain this intuitively? succeeded takes any value and creates a Decoder that ignores the Json and returns that value (if the Json is well formed). That doesn’t seem a sound way to start decoding for a real value.
Then there is the pipelining, a puzzle of type signatures that I’d imagine a javascript developer would struggle quite a lot to understand.

Wouldn’t typeclasses (Functor or Monad) allow to express this behavior in a more abstract and clear way? I know a little bit about Haskell but I’m not a seasoned user, so I’m not sure my intuition makes sense.

1 Like

It could do. You can rewrite the decode pipeline without that package by doing:

andMap : Decoder a -> Decoder (a -> b) -> Decoder b
andMap =
    Decode.map2 (|>)

userDecoder : Decoder User
userDecoder =
  Decode.succeed User
    |> andMap "id" int
    |> andMap "email" string

The andMap is a standard function. Forgive my fp theory ignorance but can it be defined to work over any monad?

This is apply or <*>, typically defined in Control.Applicative.

pure User
  <*> (field "id" int)
  <*> (field "email" string)

or something.

1 Like

Think about this pipeline like List.all. You start with true (In this case, succeed) and you apply all predicates (required) and check what is at the end.

Here you need a datatype with Applicative instance and Monoid constraint on first param. Validation package does that in Haskell.

To be fair:

  1. Names in Haskell are bad for newcomers.
  2. If you want to just use Validation, type classes do not buy you much.

But using type classes in Validation, you can easily extend behaviour. Because you write code with unknown types with certain properties you don’t need to control Decode.

I wouldn’t agree with that.

For example, here it was necessary to explain how map worked in a Functor:

This is necessary because providing a functor instance of Either essentially means declaring that “Either.map can work as map for a Functor.” You still need to know what the implementation of Either.map does in order to know what calling map will do; Functor does not and cannot help with this.

Neither Functor nor List enforce anything about the implementation of map. There’s a convention that a function named map should follow certain guidelines but it’s a convention only. Functor tells us what the type of map is, but so does List.map.

In summary:

  • Functor gives us no more compile-time guarantees about what map does or how it works than List.map does.
  • You can’t know what map does just from the fact that it’s coming from Functor, you need to know the implementation - which is why it’s necessary to explain what map does for a Functor implementation of Either, exactly like how it might be necessary to explain what Either.map does.
6 Likes

Hence, it would be fair to say - ? - that the typeclasses are useful to the extent that types naturally fall into classes, i.e. they are functionally congruent to such an extent that it’s obvious what the class functions mean in each case.

It’s clear enough that this requirement succeeds with basic types like Either and List, but maybe that alone isn’t enough to make it a desirable language feature. Maybe the “killer application” here is waiting to be discovered - like GUI elements are such a natural fit with OOP.

1 Like

Richard argues against “intuitivity” of map and I would agree with him*. Type class, interface, protocol only describe intent - not implementation.

But “killer application” of typeclass is polymorphism. With them I can freely create my custom types, implement typeclasses and get whole ecosystem of utilities.

Elm forces you to write utilities with specific types in mind. If you want to extract some common patterns you discover that you need to implement them over and over again.

I’m not sure if you need that level of flexibility in web app DSL.

  • if I could climb my ivory tower I could say that is obvious that Either maps over right part because in definition it says that Either a is a functor. But as Richard said, you could create unlawful instance.

Sure, polymorphism, loads of things you can do. But would I agree with the decisions you made, when you designed your classes and mapped them onto your types?

That’s what I mean about GUI widgets and OOP - when you make up a hierarchy of Buttons, Views, Windows etc., anyone can see how useful OOP is right there. Everyone’s seen OOP go places it isn’t needed, and have aspects that aren’t so beneficial. So the case for it is much more compelling when you have something to show for it like GUI widgets, where it’s immensely useful in a large, open problem space. Not just core types.