3
votes

I'm learning Haskell, from the book "Real World Haskell". In pages 66 and 67, they show the case expressions with this example:

fromMaybe defval wrapped =
    case wrapped of 
        Nothing    -> defval
        Just value -> value

I remember a similar thing in F#, but (as shown earlier in the book) Haskell can define functions as series of equations; while AFAIK, F Sharp cannot. So I tried to define this in such a way:

fromMaybe2 defval Nothing = defval
fromMaybe2 defval (Just value) = value

I loaded it in GHCi and after a couple of results, I convinced myself it was the same However; this makes me wonder, why should there be case expressions when equations:

  • are more comprehensible (it's Mathematics; why use case something of, who says that?);
  • are less verbose (2 vs 4 lines);
  • require much less structuring and syntatic sugar (-> could be an operator, look what they've done!);
  • only use variables when needed (in basic cases, such as this wrapped just takes up space).

What's good about case expressions? Do they exist only because similar FP-based languages (such as F#) have them? Am I missing something?

Edit:

I see from @freyrs's answer that the compiler makes these exactly the same. So, equations can always be turned into case expressions (as expected). My next question is the converse; can one go the opposite route of the compiler and use equations with let/where expressions to express any case expression?

6
What your missing is if there is any computation to be done before your case statement. Otherwise, yes, using equations is probably preferred over case statements. - Farley Knight
@FarleyKnight, what if one uses whens and let-ins along with the equations? Can you give an example where one must use case expressions? - JMCF125
No, honestly, I can't think of a "must have" situation, since you could always refactor into composition of two functions, one of them using the equations. A more senior Haskeller might find an example I can't think of, but nothing comes to mind for me. - Farley Knight
IIRC, pattern matching actually gets turned into a case statement in the intermediate language, so they're really the same thing, just a different syntax. Obviously case statements can be used as sub-expressions, which makes them more flexible, but pattern matching makes for more clean function definitions. - bheklilr

6 Answers

4
votes

This comes from a culture of having small "kernel" expression-oriented languages. Haskell grows from Lisp's roots (i.e. lambda calculus and combinatory logic); it's basically Lisp plus syntax plus explicit data type definitions plus pattern matching minus mutation plus lazy evaluation (lazy evaluation was itself first described in Lisp AFAIK; i.e. in the 70-s).

Lisp-like languages are expression-oriented, i.e. everything is an expression, and a language's semantics is given as a set of reduction rules, turning more complex expressions into simpler ones, and ultimately into "values".

Equations are not expressions. Several equations could be somehow mashed into one expression; you'd have to introduce some syntax for that; case is that syntax.

Rich syntax of Haskell gets translated into smaller "core" language, that has case as one of its basic building blocks. case has to be a basic construct, because pattern-matching in Haskell is made to be such a basic, core feature of the language.


To your new question, yes you can, by introducing auxiliary functions as Luis Casillas shows in his answer, or with the use of pattern guards, so his example becomes:

foo x y | (Meh o p) <- z = baz y p o
        | (Gah t q) <- z = quux x t q
    where 
       z = bar x
4
votes

The two functions compile into exactly the same internal code in Haskell ( called Core ) which you can dump out by passing the flags -ddump-simpl -dsuppress-all to ghc.

It may look a bit intimidating with the variable names, but it's effectively just a explicitly typed version of the code you wrote above. The only difference is the variables names.

fromMaybe2
fromMaybe2 =
  \ @ t_aBC defval_aB6 ds_dCK ->
    case ds_dCK of _ {
      Nothing -> (defval_aB6) defval_aB6;
      Just value_aB8 -> (value_aB8) value_aB8
    }

fromMaybe
fromMaybe =
  \ @ t_aBJ defval_aB3 wrapped_aB4 ->
    case wrapped_aB4 of _ {
      Nothing -> (defval_aB3) defval_aB3;
      Just value_aB5 -> (value_aB5) value_aB5
    }
3
votes

The paper "A History of Haskell: Being Lazy with Class" (PDF) provides some useful perspective on this question. Section 4.4 ("Declaration style vs. expression style," p.13) is about this topic. The money quote:

[W]e engaged in furious debate about which style was “better.” An underlying assumption was that if possible there should be “just one way to do something,” so that, for example, having both let and where would be redundant and confusing. [...] In the end, we abandoned the underlying assumption, and provided full syntactic support for both styles.

Basically they couldn't agree on one so they threw both in. (Note that quote is explicitly about let and where, but they treat both that choice and the case vs. equations choice as two manifestations of the same basic choice—what they call "declaration style" vs. "expression style.")

In modern practice, the declaration style (your "series of equations") has become the more common one. case is often seen in this situation, where you need to match on a value that is computed from one of the arguments:

foo x y = case bar x of
            Meh o p -> baz y p o
            Gah t q -> quux x t q

You can always rewrite this to use an auxiliary function:

foo x y = go (bar x)
    where go (Meh o p) = baz y p o
          go (Gah t q) = quux x t q

This has the very minor disadvantage that you need to name your auxiliary function—but go is normally a perfectly fine name in this situation.

2
votes

Case expression can be used anywhere an expression is expected, while equations can't. Example:

1 + (case even 9 of True -> 2; _ -> 3)

You can even nest case expression, and I've seen code that does that. However I tend to stay away from case expressions, and try to solve the problem with equations, even if I have to introduce a local function using where/let.

1
votes

Every definition using equations is equivalent to one using case. For instance

negate True = False
negate False = True

stands for

negate x = case x of
             True -> False
             False -> True

That is to say, these are two ways of expressing the same thing, and the former is translated to the latter by GHC.

From the Haskell code that I've read, it seems canonical to use the first style wherever possible.

See section 4.4.3.1 of the Haskell '98 report.

1
votes

The answer to your added question is yes, but it's pretty ugly.

case exp of
  pat1 -> e1
  pat2 -> e2
  etc.

can, I believe, be emulated by

let
  f pat1 = e1
  f pat2 = e2
  etc.
in f exp

as long as f is not free in exp, e1, e2, etc. But you shouldn't do that because it's horrible.