Wondering about Typeclasses

Sure, but typeclasses don’t help with that.

Here’s the same function in Haskell, using the Num typeclass:

mult :: Num a => a -> Float -> Float
mult a b = a * b

Here’s what ghc says about it:

Mult.hs:2:16: error:
    • Couldn't match expected type ‘a’ with actual type ‘Float’
      ‘a’ is a rigid type variable bound by
        the type signature for:
          mult :: forall a. Num a => a -> Float -> Float
        at Mult.hs:1:1-36
    • In the second argument of ‘(*)’, namely ‘b’
      In the expression: a * b
      In an equation for ‘mult’: mult a b = a * b
    • Relevant bindings include
        a :: a (bound at Mult.hs:2:6)
        mult :: a -> Float -> Float (bound at Mult.hs:2:1)
  |
2 | mult a b = a * b
  |                ^

So going back to the original question:

This affects both Haskell and Elm equally, regardless of typeclasses, because both languages use Hindley-Milner type unification rather than subtyping.

Type unification lets you take a number -> number -> number (in Elm) or Num a => a -> a -> a (in Haskell) function and apply it with a Float for either argument, but it still considers a number -> Float -> Float function type-incompatible with a number -> number -> number one (and likewise, it considers a Num a => a -> Float -> Float function type-incompatible with Num a => a -> a -> a). Subtyping could change that, but allowing subtyping would seriously degrade compile times in either language.

Typeclasses don’t help here!

3 Likes

I don’t mind the current Elm choice of Type.map.

This is my main take away of what’s desirable but missing currently:

Is there another approach that Elm could take to obtain this? Does it have to be only a) Haskell Typeclasses or b) No Haskell Typeclasses?

Since Elm did forge its own path with semver enforcement in packages.

  • If we export a map with incompatible signatures, could it be rejected?
  • If we export a transform or fromElement, could they be strongly advised to use certain names?
  • Something else?
4 Likes

One approach that I think was suggested in a previous discussion is to allow custom types to implement the defined set of “typeclasses” (number, comparable, etc), but not to allow for creating custom type classes.

At least one problem with that: Unlike Haskell’s syntax, the magic type variables of Elm make it inconvenient to specify that a type should implement several type classes at once. There is compappend to express both comparable and appendable simultaneously but no other combinations. (Because number implies comparable, and number and appendable have no common types.) If custom types were to be admitted, you’d need to enforce these restrictions or create new ad-hoc combination classes.

(The magic type variables really are the most inelegant part of Elm. Haskell’s solution makes so much more sense.)

2 Likes

This is a good point, and a good solution would involve addressing that problem.

Another possible approach is to completely remove these variables and require the passing of things like String.compare to dictionary operations.

I think this would be a good idea:

  • number: we could easily have an extra set of operators say for floating point math (I think Gleam has .*, .+, etc)
  • appendable: I think if strings didn’t have an append operator that would be fine.
  • comparable: We would just have to add relevant functions Float.lessThen (elm-units already does it and the code is fine, if a bit verbose feeling).
1 Like

This is starting to sound like: Just for fun: What if there were no type classes at all?

I find it hard to remember everything I want to reply to so I think I’ll just make one post replying to all the new comments.

Results are used in situations where you do multiple operations, any of which may fail. If it failed you just pass on the failure. Therefore it makes sense that you operate on the Ok value, just as Maybe.map operates on the Just value.

Eithers do not have this because they are simply Left or Right—no connotation about which means what. It’s just confusing when Eithers are used when Results are more apt.

I agree with you here. Neither typeclasses or extensible records cause these problems, and they actually solve many problems that would be otherwise hard to solve without them. The argument has virtually no weight because in both cases, abusing the feature will just backfire on you and possible the users of your package (if you are making a package). Much better is to just think critically about whether you are using the typeclass/function accepting an extensible record for its intended purpose.

Am I missing something? Are you referring to the convention, or maybe to the fact that the code assuming the signature wouldn’t type-check? Because I don’t see any reason why I can’t export a map function from a module with whatever signature I like. If I’ve missed something fundamental please tell me!

I see your point. I think I missed the mark with typeclasses there. Though in my defense I wasn’t really referring to Haskell. I meant you would have a number typeclass that requires the implementation of say, toFloat, so you could have number be truly polymorphic: for Float it would be identity and for Int it would be the existing toFloat. That’s what I meant when I said number is basically useless.

Interesting idea though I think that it would be counterproductive. A better solution IMO would be to have simple, usage-oriented typeclasses like Mappable as opposed to the computer science mumbo-jumbo of Haskell.

100% agreed. It’s an abuse of the syntax for type variables.

Yes, it certainly is. I don’t want this conversation to transform into something about alternatives to typeclasses. I really just think a debate as to whether typeclasses are worth it should be the limit of this thread.

Ah, I was referring to the convention. I interpreted “all instances” as “all situations” rather than “all typeclass instances” - so I see what you mean now!

There are ways in Haskell to auto-derive Functor so that is probably not the best example.

Your are right that the code a library author has to write is probably largely the same but for the consumers of your library it is very different.

Not all abstractions are good or lead to readable code and Elm might have a point in disallowing a huge operator-zoo (lens anyone?) but there are a couple of by now rather obvious good abstractions and Functors are among them (there is a reason even in Elm you have maps everywhere)

1 Like

“The syntax for using the implementations” is more than just syntax. It actually lets multiple types pass through. A contrived example would be fmap show to convert a boxed type to a boxed string. I had issues with this type of thing when trying to write functions to act on numbers.

1 Like

This is more a lack of higher-kinded types than type classes, though. I would be fine with writing

mapIntsToStrings : ((a -> b) -> f a -> f b) -> f Int -> f String
mapIntsToStrings fmap = fmap String.fromInt

So you get the added abstraction power without the implicit parameters type classes essentially provide. (And you need these for (your example using) type classes anyway.)

1 Like

This is always a lovely subject, if only because of the insights you get from all the comments.

But honestly, I think the thoughtful language design and its resulting usability are Elm’s greatest assets. Being cautious with the typeclasses is part of that.

Anyway, purescipt is also a beautiful language. It has all those things Elm doesn’t. There is a place for both

2 Likes

A good programming language only has either tagged unions or custom typeclasses - not both.

A good post does not have both a bold claim and an explanation?

3 Likes

I have a gut feeling that you have very specific and limited list of “good” languages …

+1. Through reading the conversation I’ve gained a lot of understanding about the tradeoffs and nuances.

For me it’s the exact opposite - whenever one of the two is missing I find the other to fall short in some scenarios I face. I honestly don’t understand why you should be limited like this, they are two non-overlapping solutions to significantly different problems.

2 Likes

This conclusion makes no sense.

Tagged unions without custom typeclasses = your types feel 2nd-class compared to the built-ins

Custom typeclasses without tagged unions = how do you even do functional programming here??

2 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.