In your screen shot there’s like 20 different map
functions. If we had a Functor
type class, there would be 20 different “implementations” (not sure what it’s called when you “fulfil” the functions of a type class) of fmap
instead (one per data type: Maybe
, List
, Set
, etc). Or am I mistaken? Out of curiosity (I’m not deep into Haskell), what’s the difference?
I like how Elm is positioned as a small language that is beginner friendly, but is also strong on getting the job done. It makes a clean break from the accademic functional languages, such as ML, Haskell and OCaml and is better in some ways for it. Compared with those languages the syntax has been cleaned up and made very elegant and minimal, as has the language feature set.
It is true that type classes would bring a lot of power to the language, but I also wonder how often we really need it? It would be most useful for writing packages. Every time I have wanted more powerful features in Elm I have had to find another way, and just write out a new set of map
and andThen
functions and some other common FP patterns. It may mean duplication of code, but it has never been hard or time consuming to do. So on the balance between power and simplicity and effort, I think we are paying a small extra effort to keep things simple. More complexity and power would save a small effort, but add cognitive overhead to the language, programs could be far more subtle and difficult to understand. Elm is emphasising simple code and keeping the focus on building something rather than being too clever - I am someone who always gets drawn into the trying to be too clever trap, and I love how Elm frequently stops me from going there.
The Elm community is diverse in that is has brought together Javascript web programmers, and people with a more formal software engineering (myself) or accademic background, as well as complete beginners. It is great that Haskell exists and has enabled so much FP language research to happen, but Elm is where that translates into a practical, lovable, day-to-day code production language. I would call Scala a compromise language; it tries to be all things to all people by including too many language features. Elm is not a compromise language, it is a carefully chosen and minimal set of language features. I like to think of Elm as a Goldilocks language, well positioned and ‘just right’ for its use case.
When discussions like these arise, they can be useful and enjoyable even when we know the (extremely) likely outcome is that Elm will not change.
With that in mind I think it’s incredibly important to be honest and open when discussion the pros/cons of something like type classes, and also earnestly consider how their implementation might work in Elm specifically. Simply cut-and-pasting Haskell’s implementation is unlikely to be a good fit for a hypothetical Elm-with-typeclasses, but how might we make it work whilst still aligning with Elm’s goals.
With that in mind I’d like to pick apart a few of the things @rupert and others are saying:
It would be most useful for writing packages. … [I] just write out a new set of
map
andandThen
functions and some other common FP patterns. It may mean duplication of code…
The benefit of type classes in this case is not about saving you code duplication, its about codifying meaning. map
means something very specific in Elm, and when a type implements a function called map
but doesn’t meet the expectations we have of a function with that name, it is a bad developer experience. Typeclasses solve this problem by having being the canonical “this is what map means”, and when a type implements Functor
we know how map
works.
@wolfadex There’s also nothing stopping me from writing a typeclass in Haskell that’s equivalent to
fmap
calledftransform
.
Yes there is. Just because our hypothetical Elm-with-typeclasses has typeclasses, there is nothing to say typeclasses must be user-definable. This ties into the general idea that typeclasses make the language insurmountably complex to beginners and are a complicated feature that must be avoided at all costs.
My perspective is this:
- Typeclasses are confusing because they have weird names at its not clear how they’re really useful or applicable until you’ve used them for a while
- Typeclasses are confusing because there are an infinite number of them, that may do similar or the same things, with no real standardisation over what they mean outside of the standard library.
There’s nothing stopping our hypothetical Elm from simply having Equatable
, Comparable
, Appendable
Mappable
, and Thennable
as bonafide typeclasses and providing a mechanism for implementing those classes for any type whilst also not providing a mechanism for custom typeclasses.
Elm already renamed monoid to appendable
, bind to andThen
, and so on. If there are genuine concerns about learnability then we should discuss how to solve those problems not simply hand wave away typeclasses as “too hard for beginners”.
More complexity and power would save a small effort, but add cognitive overhead to the language, programs could be far more subtle and difficult to understand. Elm is emphasising simple code and keeping the focus on building something rather than being too clever
There is this common idea that typeclasses are all complexity and all overhead and no benefits beyond appeasing genius Haskell programmers. If typeclasses aren’t simple, why is the discussion never “how do we make typeclasses better” the same way elm made error messages better. Before elm the consensus was error messages are hard work to get right and not really worth the effort, most language designers would agree that that’s not the case anymore but until Elm really challenged that thinking that’s how it was. Why are typeclasses where we draw the line and accept defeat?
With every discussions about adding features to language we need to understand two points:
-
There are no good, objective measures of language quality. There is no threshold between easy and hard, there is no fine balance to strike between simple and powerful. There are only opinions and spectrums.
-
In many languages there is a free flow of users between communities. Sometimes you need use Java or some corporate-chosen language, but most of the time, people in language discussions on internet are free to pick their favourite language.
These two conditions leads to community wide opinions about what is good and what is bad for language. People with same opinions flock, people who think different go to other communities and we live in happy bubble of “simple yet powerful, rigorous yet pragmatic, easy yet expressive” languages.
There is no reason that extensible records are simple and needed in Elm, yet type classes are too complex and probably superfluous.
Just like old javascript programmer must be lectured why isLoading
is bad and “custom types” are the way, experienced elm programmer must actively look what problems type classes solves.
If the function doesn’t work the same as all other map
s, it’s not map
. It should be under a different name.
Good point. However I don’t see how the HKTs need to be in the language syntax when Elm already knows how many type parameters the “HKT” has. It would be an implementation detail, especially considering that all of the types in a type signature must be of kind *
(regular type).
I agree that they aren’t immediately applicable to the real world, but not that they have no use. They abstract away the implementation of, say, map
, and just let us think about acting on the value inside. My issue with Elm’s gang of map
s is that you lose that abstraction by having to explicitly state what type is being map
d.
I agree 100% about this. Considering Elm’s standpoint, I think they should just be named things like Mappable
(Functor), Thenable
(has the Monad bind operator, named after Maybe.andThen
), Iterable
(Traversable), etc. Haskell comes from CS theorists that have fancy names for all these things; Elm focuses on the real-world application and the names should come therefrom.
Agree! Elm already has weak and watered-down typeclasses in the form of these magic types. The first step toward real typeclasses would be a constraint syntax like Number a => a -> a
, as opposed to number -> number
which feels like a kludge using the existing syntax to mean something different.
No. There would be the one fmap
function from Control.Functor
(reexported in Prelude
IIRC). Haskell solves the Functor constraint on the type signature of fmap
at compile-time.
I think we reached a point where you just need to brush your knowledge of Haskell. On one hand we have multiple examples of languages that implement or “mock” HKT to be able to have type classes - on the other hand we have your intuition “Elm should work without it”.
When you define an instance you need to pass type of specific kind. Haskell (or Elm) can’t “guess” and fill in holes. Look for example how look instances of Either
for Functor
and Bifunctor
.
instance Functor (Either a) where
fmap _ (Left x) = Left x
fmap f (Right y) = Right (f y)
instance Bifunctor Either where
bimap f _ (Left a) = Left (f a)
bimap _ g (Right b) = Right (g b)
You can’t fully apply all type parameters, so you need to be able to distinguish * -> *
and *
.
OK, I’ll concede that. I still don’t see how HKT syntax is necessary as you used none in your example, nor why you needed (Either a)
when you don’t use a
in the instance, but this is a sidebar and not very important in the general discussion so we might be better off just letting it go.
Another very painful point here: trying to convert a number
to a String
…
And what is this???
https://github.com/elm/compiler/issues/30#issuecomment-9085288
Evan says “I plan to implement typeclasses, so read
would be the ultimate goal.”
I know this is old, but talk about mixed messages!
This is sort of a theme when working with Elm. For myself, it meant I could internalize what List.map means without first having to unpack the conceptual underpinnings of… Endofunctors?..
Anyways, I sort of like this trade-off, but I get why it isn’t for everyone
Acting on the value inside. For a list, there are multiple values, so the operation works on all of them. For a Maybe
, there might not be a value, in which case it stays Nothing
.
I think that there’s a lot of scary stuff about computing theory, but when you stick to what’s actually happening, it gets a lot simpler.
I don’t understand – I thought you had to define instances like this for every data type?
instance Functor Maybe where
fmap f (Just x) = Just (f x)
fmap f Nothing = Nothing
Elm:
-- in Maybe.elm
map f maybe =
case maybe of
Just x -> Just (f x)
Nothing -> Nothing
You do, but the module isn’t required to expose the implementation of fmap
. By and large they do, but you can implement Functor
without users being able to import Maybe (map)
.
Here’s another way Elm could (potentially) improve: require instance functions to both:
- be defined at the top-level
- be publicly exposed.
So this:
module Maybe exposing (Maybe(..))
instance Mappable Maybe where
map f (Just x) = Just (f x)
map f Nothing = Nothing
wouldn’t be allowed but:
module Maybe exposing (Maybe(..), map)
map : (a -> b) -> Maybe a -> Maybe
map ...
instance Mappable Maybe where
map = map
would be required.
Yes, but that’s defined in the module for the data structure. For the application programmer it’s just fmap
.
I want to make clear that this isn’t the main benefit of typeclasses. The main benefit is polymorphism. Typeclasses work like OO interfaces, making one function with different implementations in different classes. I don’t know your level of familiarity with OO, but OO interfaces allow you to use the same function on objects that may be fundamentally different—they only need to have the functions specified in the interface. Same with typeclasses.
hehe, thanks for reminding me
I mean in the case where someone new is, for example, want to turn a string into an integer, and then do something with it, if it’s even.
Maybe.andThen
might be a new tool to learn, to get the job done.
I think it’s pretty easy to grasp how this single function works, instead of looking up the haskell-docs on Maybe, understand the fact that it adheres to the monadic axioms, and that I can, for that reason, “flatmap” over it.
Does it matter? If fmap
for Maybe
is just Maybe.map
, why not use fmap
?
Completely agree with you here. That’s why, instead of calling it Monad (which instill fear at the mere thought of it), a more friendly name that focuses on the real-world application would be Thenable.