1

I tried to find out how Haskell is able to resolve types from a guarded equation where no type is specified through the web.

Like in e.g. following function definition, ghci is able to resolve the type and tells me exactly what it is.

fun a b c
| a == c = b
| b < 0 = a+b
| otherwise = c

How does it do this? I know how this works for if-then-else constructs (basically: start with generic version, add constraints) but I am wondering which extra steps are needed here?

d3Roux
  • 307
  • 1
  • 10
  • 1
    Guards are sort of like if/then/else anyway, at least conceptually, so I'm not sure why you think something extra is in need of explanation here. – Robin Zigmond Nov 10 '19 at 18:40
  • 2
    Possible duplicate of [What part of Hindley-Milner do you not understand?](https://stackoverflow.com/q/12532552/791604). – Daniel Wagner Nov 10 '19 at 19:21
  • 1
    Guards can be in fact be understood as simply _syntactic sugar_ for `if`-expressions (which in turn can be reduced to `case`, which also allows for pattern guards). GHC actually typechecks before all desugaring, but that's an implementation detail and mostly done to get better error messages. – leftaroundabout Nov 10 '19 at 20:34

2 Answers2

2

fun has three arguments and a result. So initially the compiler assumes they might each be a different type:

fun :: alpha -> beta -> gamma -> delta         -- the type vars are placeholders

Ignoring the guards, look at the rhs result of the equations: they each must be type delta. So set up a series of type-level equations from the term-level equations. Use ~ between types to say they must be the same.

  • The first equation has result b, so b's type must be same as the result, i.e. beta ~ delta.
  • The third equation has result c, so c's type must be the same as the result, gamma ~ delta.
  • The second equation has rhs + operator :: Num a => a -> a -> a. We'll come back to the Num a =>. This is saying + has two arguments and a result, all of which are same type. That type is the result of fun's rhs so must be delta. The second arg to + is b, so b's type must be same as the result, beta ~ delta. We already had that from the first equation, so this is just confirming consistency.
  • The first arg to + is a, so a's type must be same again, alpha ~ delta.

We have alpha ~ beta ~ gamma ~ delta. So go back to the initial signature (which was as general as possible), and substitute equals for equals:

fun :: (constraints) => alpha -> alpha -> alpha -> alpha

Constraints

Pick those up on the fly from the operators.

  • We already saw Num because of operator +.
  • The first equation's a == c gives us Eq.
  • The second equation's b < 0 gives us Ord.
  • Actually the appearance of 0 gives us another Num, because 0 :: Num a => a, and < :: Ord a => a -> a -> Bool IOW the arguments to < must be same type and same constraints.

So pile up those constraints at the front of fun's type, eliminating duplicates

fun :: (Num alpha, Eq alpha, Ord alpha) => alpha -> alpha -> alpha -> alpha

Is that what you're favourite compiler is telling you? It's probably using type variable a rather than alpha. It's probably not showing Eq alpha.

Eliminating unnecessary superclass constraints

Main> :i Ord
...
class Eq a => Ord a where
...

The Eq a => is telling every Ord type must come with an Eq already. So in giving the signature for fun, the compiler is assuming away its Eq.

fun :: (Num a, Ord a) => a -> a -> a -> a

QED

AntC
  • 2,623
  • 1
  • 13
  • 20
  • Thank you for the detailed QED! This helps me understanding how type inference works in Haskell in general a lot better. I assume that guards and if/then/else constructs are handled equivalent implicitly according to type inference. – d3Roux Nov 11 '19 at 10:04
0
fun a b c
| a == c = b
| b < 0 = a+b
| otherwise = c

Is it clear how this translates to if/else?

fun a b c
 = if a == c
      then b
      else if b < 0
             then a+b
             else c

And if we add some human-readable annotations:

fun a b c      -- Start reasoning with types of `a`, `b`, `c`, and `result :: r`
 = if a == c   -- types `a` and `c` are equal,  aka `a ~ c`.  Also `Eq a, Eq c`.
      then b   -- types `b ~ r`
      else if b < 0   -- `Num b, Ord b`
             then a+b -- `a ~ b ~ r` and `Num a, Num b`
             else c   -- `c ~ r`

If you take all these facts together the type boils down pretty quickly.

a ~ b ~ r  and c ~ r

So we know there's actually only one type which we'll just call a and rename all the other types variables in the facts.

Num a, Eq a, Ord a

As a small cognitive savings we know Ord implies Eq so we can end up with constraints Num a, Ord a.

All that swept the mechanics - leveraging of implications such as (==) t1 t2 ~~> t1 = t2 - under the rug but hopefully in an acceptable fashion.

Thomas M. DuBuisson
  • 64,245
  • 7
  • 109
  • 166
  • Looking at your workings, I rather think the nesting for `if-then-else` makes it harder to see the program's logic or its type inference. That applies especially if there are type errors: the compiler will point to the errors in the guards source lines, not to the desugarred version. – AntC Nov 10 '19 at 21:51
  • @AntC I don't understand what you're getting at. The asker mentioned they know how type inference works in the face of if-then-else so I merely sought to show how guards don't require any additional knowledge wrt type checking. I wasn't advocating replacing guards in real programs or as a naive compilation step. – Thomas M. DuBuisson Nov 10 '19 at 22:09