In retrospect, was burning bridges a mistake?

Making traversable and foldable available from the prelude was a big win for me, since I use those functions everywhere. Making traversable a superclass of foldable is something that I don’t consider a big deal either way – every instance of traversable should have been an instance of foldable anyway, and whenever this was not the case, I found it a rude surprise, but also just a minor papercut. Now, I don’t have to worry about that papercut anymore, which is nice, but not a big deal.

4 Likes

My experience is consistent with that of dixonary and Graham Hutton: the 200+ students we teach at Utrecht University every year also don’t really seem to have issues with it.

As for having methods such as any, all, lookupMin, lookupMax etc as members of Foldable: I actually find it very convenient. Even though many of these functions could be written as ‘f . toList’ or the like. I find that having to manually squeeze in the toList conversions everwhere is very distracting from the main idea of the algorithm. Hence, I’m happy that resolving to foldable I don’t need to. Furthermore, it is a nice bonus getting “the right” (more efficient) implementation for s.t. like lookupMin when it is available by default / without me having to worry about it again.

By the way @BurningWitness: in general you cannot implement TraversableWithIndex using just traversable + accumlator. That requires the index to be an instance of Enum. (and indeed, the default implementation form indexed-traversable only applies when the index is of type Int).

9 Likes

If you design your course carefully and you design your examples carefully, …

the tuple instance for these things is bizarre, even though there’s a rationale behind it.

Again, thank you for pointing to hard evidence. I still get the feeling from GH’s wider remarks GHC ‘could have done better’ and could have delivered an easier experience for learners. (Not every learner gets the opportunity to be taught by Graham.)

What’s also bizarre about ‘the’ tuple instance is why there’s an instance for a two-element tuple, but not 3,4,5,6,… Can someone explain the rationale behind that? (Yes of course I could declare them; but equally I could declare for a twople if that’s what I wanted – which I don’t.)

Yes sure. To be clear (for those who weren’t there at the time), a great deal of the FTP was welcome and sensible. Given how much Haskell tutorials are oriented around Lists, and that Lists had been treated specially (map :: (a -> b) -> [a] -> [b]; length :: [a] -> Int; foldr :: (a -> b -> a) -> a -> [b] -> a), suddenly dropping learners into higher-order typeclasses with all their abstruse error messages and “bizarre” instances seems … emm, poorly motivated. (map remained as dedicated to Lists, why not length? That length is applicable to ‘the’ tuple still surprises me; that it returns 1 is bizarre; that it’s not applicable to wider (I don’t say “longer”) tuples is a relief.)

How exactly does one convince (presumably) mathematically-aware students to “get out of the mindset” that e.g. the division function can fail?

Exactly, not to forget ++ as well.

This is very interesting to me! I would not have expected that. Helpful to have the experience shared here, thanks.

1 Like

That’s interesting because on the one hand, Haskell seems to be notoriously difficult to learn and on the other hand teachers seem to find it easy to teach …
Where are these hundreds of proficient Haskell students ?

(Alternatively they just don’t complain because they have no interest in Haskell whatsoever and just find someone to do their homework for them).

1 Like

They’ve learnt so fast (because taught so well) that they never need ask questions on StackOverflow nor reddit; they don’t participate in discourse because only strugglers hang out there; they’re busy working using Haskell in industry, and find GHC has exactly the functionality they need, so never need ask for enhancements. What enhancements come along, it turns out the Steering Committee knew exactly what they wanted by mind meld.

And in their coffee breaks they program (in Haskell of course) space lasers (in orbit on spaceship USS Dependent Nirvana) to shoot down all those pigs floating across the horizon.

Or … they found the Intro to Haskell/FP module something of an out-of-body experience; the programming lessons passed from the notes of the lecturer to the notes of the student to the course exam papers without troubling the minds of either; they got their grades; they moved on without a backward look. To the extent they reflected on it at all, they thought it rather weird for a so-much talked-about language to not have a records system. They presumed that must get taught in the second semester module.

1 Like

To the extent they reflected on it at all, they thought it rather weird for a so-much talked-about language to not have a records system. They presumed that must get taught in the second semester module.

So if you really want to impress us…port that wondrous records system to GHC, so we can all see what we’ve been missing for so long! Considering just how often you’ve promoted it, it should be absolutely awesome!

1 Like

Many of our students do indeed think (functional programming/programming in Haskell) is difficult. They learn many concepts/ideas (e.g. recursion, algebraic data types, higher-order functions, type inference, equational reasoning, typeclasses, some IO/monadic programming) that are new/different to them. There is simply a lot to learn, and hence they think this is difficult. I can sympathize with that. (By the way: I never claimed FP/haskell was easy to teach: since there is a lot to learn, so there is a lot to cover in a relatively short time; that sometimes makes it challenging).

However, I don’t particularly see why they should find the notion of a Foldable typeclass so strange/difficult. If you can accept that there is a “Num” typeclass that expresses that there are types that look like numbers (and thus provides functions to work with such things) then is it really all that surprising that there is a “Foldable” typeclass that models “container-like types” with functions that allow you to combine/flatten/reduce the container into some value? The hard part is actually explaining the idea of a fold (i.e. a higher order function), not so much that you can define something like fold/foldr/foldMap for Lists as well as some other data structure.

Of course there are some weird instances for Foldable. We don’t explicitly spend time on those, and in all likelyhood our students will not try to use any of those instances themselves throughout the course (either since they don’t need them to to solve their homework questions, or since they don’t fully realize that they could have (attempted) to use these instances). Whether they will hit those issues later I don’t know, but at least I don’t think it is a “beginner” issue.

In all honesty; issues in terms of tooling (e.g. getting a working compiler, editor etc) are much more challenging to deal with as a teacher: if students have an frustrating start it is difficult to motivate them afterwards. Moreover, it is time consuming to help students to get something to work, since every time it is a different puzzle you have to solve to figure out what is wrong. Luckily things have improved quite a bit in the last few years, and I hope they will continue to do so in the future :).

11 Likes

Fair enough. Having said that I don’t think FTP is bad because of beginners, but just because it makes my life more difficult ;-).

1 Like

A precis of our lecture on the topic:

Mathematical division (over, say, the reals) is not a function, because it violates the property that every element of the domain (say, pairs of reals) maps to exactly one unambiguous value in the codomain.

To make division into a function, we can either exclude (x,0) from the domain, or make the codomain a union of the reals with a singleton of some sentinel value indicating failure (e.g. { ⊥ } ). The corresponding pattern in Haskell would be either to restrict the input (via a newtype) or add a sentinel value to the return value. The latter is more common, and Maybe is the standard vehicle.


I’m sure you know all of the above! But this is more or less the exact formulation that is used to explain it to students who have (at that point) less than 3 weeks of functional programming knowledge, and they are able to take this in their stride as well. This is off topic though, so I’d recommend we don’t go on about it :slight_smile:

2 Likes
newtype NonZero a = NonZero a

(/) :: Fractional a => a -> NonZero a -> a
(/) x y = ...

unitNonZero :: Num a => a -> NonZero a
unitNonZero x | x /= 0 = NonZero x

bindNonZero :: Num a => NonZero a -> (a -> NonZero b) -> NonZero b
bindNonZero (NonZero x) k = k x
divide :: Natural -> Natural -> Natural
divide x y | x < y = 0
           | otherwise = 1 + divide (x - y) y

divide x 0 = (\ y -> 1 + divide (x - y) y) 0  {- x >= y -}
           = 1 + divide (x - 0) 0)            {- application -}
           = 1 + divide x 0                   {- (-) identity -}
           = 1 + ⊥                            {- (divide x 0) regresses infinitely -}
           = ⊥                                {- (+) strict -}

…hence you either accept that division can fail:

(/) :: Fractional a => a -> a -> a

…or you make it annoyingly explicit that it doesn’t:

  • (/) :: Fractional a => a -> NonZero a -> a
    
  • (/) :: Fractional a => a -> a -> Maybe a
    

That would have been an interesting experience to rewrite everything in Haskell which relies on division to use the more explicit style for use in that course - the students must have been very impressed!


This is off topic though, so I’d recommend we don’t go on about it.

Agreed.

Well, I think the specific hard part is explaining how a function can take a function as argument. I had the advantage of learning about foldr before FTP, back when it was dedicated for Lists.

(And I appreciate my learning experience is anecdata, then for full disclosure: I’d dabbled in LISP many years before, so higher-order functions didn’t seem so bad. Nevertheless, Partial application made me feel a bit wobbly. At least I could get that secure in my mind before …)

This claim is a great surprise to me. The members of the Num class (at least the ones in the Prelude) are concrete, and the methods within Num are hugely familiar.

Constructor classes have a kind of ‘ghost in the machine’: the methods can’t mention anything about the type of the contents. (For example to say it must be in Ord for a BTree or Rose Tree as containers.) And this is a total annoyance – so much so there have been many proposals over the years to somehow smuggle a constraint on the contents inside Constructor classes/or at least acknowledge the contents will have some constraint.

Teaching the Foldable class as is today, you’re introducing three concepts all at the same time:

  • That you can abstract out the constructor vs the contents;
  • That you can pass functions as arguments to methods, to act on the contents of unknowable type;
  • That you can abstract out the constructor to some generic constructoralike of constrained but essentially unknowable type.

Yes, eventually a Haskell programmer has to understand all that together. I don’t see a learner should be expected to triple-jump before they can walk.

Anybody who thinks burning bridges was a mistake because of excessive overloading should spend some time programming in Elm. From my perspective the only mistake with burning bridges was that it stopped a bridge short. I’m continuously annoyed fmap and map have not been unified, to the extent that I overuse <$> so I don’t have to write fmap.

5 Likes

I’m continuously annoyed fmap and map have not been unified […]

It could have been worse:

In Lazy ML those functions are more simply named mapn (for n > 0, with map1 abbreviated to map).

1 Like

I developed the habit to pretend map does not exist, just write fmap everywhere. That extra character is not so much of a nuisance.

2 Likes

This probably outs me as a no-more-than-moderately-experienced Haskell dev, but I have not been able to form a consistent principle to apply on whether to prefer fmap or map—or in the context of pairs/Either, fmap or second.

In favor of map: it makes it clear that you believe you are working with a list, and if you are in the middle of a refactoring that, say, wraps the list in a Maybe, accidentally forgetting to change out map results in a compiler error closer to the place I want it to be. Also, when reading code that walks through some complex types, using monomorphic functions like map is more revealing of what part of the structure that code is working with than using a nest of identical fmaps.

In favor of fmap: if you are in the middle of a refactoring that replaces the list with a NonEmpty, fmap is one fewer change you need to make in the first place. Also, when writing code that walks through some complex types, you can often spam fmap and get something that does the right thing without having to think about it too hard.

I haven’t been able to pick a side and stick with it; I tend to go different ways on a case-by-case basis by intuition, and I always feel a little bad about it.

5 Likes

This isn’t (yet) idiomatic Haskell, but you can get all benefits of map that you want, without needing it to be a separate function; use type applications and write fmap @[] or fmap @(Either _). It also works to distinguish which part of the structure you’re working on; fmap @[] . fmap @Maybe

6 Likes