I came to a similar insight by the corollary: a sound type system makes the edge cases obvious. When we make the type of a partial function total by introducing an optional value as a result there's an opportunity to develop an intuition that this is going to make my code harder to reason about. Assuming I fix all the warnings about incomplete pattern matches I know that users of this code will have to handle all of the edge cases I'm introducing. Therefore I might take another approach and introduce a type as my pre-condition: the type of non-empty lists. You can't even call my function unless I can be assured there's at least one element in the input. I haven't even had to look at the implementation to reason about my code yet.<p><pre><code> head :: [a] -> a
-- versus
saferHead :: [a] -> Maybe a
-- versus
safeHead :: NonEmpty a -> a
</code></pre>
The first function is <i>partial</i> because we'll get an exception if we give it the empty list.<p>The second function is total but annoying to use because we're telling all the code that uses the result to check for two cases (it's an error not to).<p>The last one forces the responsibility on the <i>caller</i> to provide a non-empty input.<p>That was probably one of the more frustrating aspects about learning to program Haskell as someone who has been programming for more than fifteen years when I started. It revealed to me in stunning detail all of the edge cases that almost every other language I've used actively hides from me.<p>It doesn't absolve you of having to think about edge cases: even Haskell throws run-time exceptions. However it does give you tools to think about many of those edge cases up-front and in a direct way.<p><i>Update</i> Added a trivial example to demonstrate "partial," etc.
Forget the performance or the fancy memory management stuff, this is one of the best things about Rust: it makes you deal with edge cases by default (while allowing you to write code in a largely imperative or functional-lite style).<p>It's why I think it competes for market share with some of what is currently Java/C# and similar languages. A lot of people who use those languages care about correctness, and Rust is big step up in this regard.
I am not sure this is the only value, but it is generally true. And this is one of the great advantages of haskell.<p>By the way, now that I know some haskell, I see this edge case sloppiness all the time and it is really annoying. For example, here is something that annoyed me just recently. You go to yahoo finance and there they will show you the revenues of a company as well as the revenue growth from past year. But sometimes this revenue growth number is not available. For example, the company may not have existed past year, or perhaps it was not public past year and thus it did not publicly disclose its revenues. When they cannot compute revenue growth for these reasons, yahoo finance will helpfully show revenue growth as "N/A", i.e., not available.<p>That seems all nice and logical, but I noticed that yahoo finance tended to show that revenue growth was not available for many established public companies for which they should have the data. I looked at it more closely and noticed with shock that if the revenue growth of a company was about 0%, yahoo finance would still show it as "N/A"! So some programmer just decided to use 0 as not available and just erased a lot of useful information from their system. Now when I see a company that has N/A as revenue growth on yahoo finance I have to go and check whether the revenue growth was zero, or it was truly unavailable. As I said -- really annoying.<p>This is of course a textbook case for Haskells Maybe concept. You can have a type that is a Maybe Int, and that means it have a value or it can be Nothing. Thus, you encompass the edge case of not having the value available, without using an embarrassing hack that deletes real data. But of course Haskell gives you more power than just using Maybe. You can create a type that can encompass all kinds of edge cases and information about them. For example, you can embed a reason why the revenue growth data was not available, if that is the case.
They say that, and then turn around and show an example using IEEE-754 Doubles. You've got two infinities and two "not a number" values. What your type system thinks is a number isn't even necessarily a number. Maybe.<p>Better be sure to call the "isNaN()" and "isInfinite()" methods all over the place, because those are the worst edge cases of all and your language provides no help in detecting or avoiding them. It's like having 3 or 4 other None values, where they don't even use the same system as your standard Option type.<p>If I had a nickel for every time I saw "NaN" appear on a webpage or a web browser console log ... We throw up our hands and say, yeah, well, JS is terrible -- but no <i>static functional</i> language I know of is any better.
Paul Snively (the author of the linked reddit post) also gave a great talk entitled 'Typed FP on the Job - Why Bother?' at LambdaConf a couple years back, that spoke strongly to me:<p><a href="https://www.youtube.com/watch?v=8_HsFrXhZlA" rel="nofollow">https://www.youtube.com/watch?v=8_HsFrXhZlA</a><p>> <i>And now you can do the most important thing you can do with any piece of code... stop thinking about it! Go home! Pet the cat! Watch House with the wife! ... I love Friday deployments ... you know why? Because I know what my code is going to do before it runs!</i>
Most languages rely on coding patterns for code safety. This is merely a <i>best practice</i>, usually involves writing multiple lines of code, and is merely a convention.<p>Functional languages have many of these patterns already encoded into common functions, like map and fold. As functions their correctness can be considered mathematically proven. Would you rather rely on <i>convention</i> or the compiler?<p>Consider adding on top of functional safety/correctness type safety with a functional language implementing a strong type system, like F#. Sure, F# is only functional first, not purely functional, and probably all type system have some sort of implementation edge issues. For most practical purposes you can consider the added level of type safety imposed by the compiler also to be mathematically provable.
I worked with Paul at a previous job and really enjoyed it. Such a great mind and passion for functional programming. Would love to work with him again (maybe in a different context).
Edge case removed:<p><pre><code> for i=0 to 10 {print(a[i])} vs for x in a {print(x)}
</code></pre>
With functional programming it gets even better:<p>a.filter {}.map{print}<p>Pseudo code, of course. I list several “functional“ examples in Swift here:<p><a href="https://github.com/melling/SwiftCookBook/blob/master/functional.md" rel="nofollow">https://github.com/melling/SwiftCookBook/blob/master/functio...</a><p>Functional programming is also a higher level language. map, reduce,filter, flatten, flapMap, drop, take, zip, ...<p>Learn the concepts in one language and they can be used in another functional language.
For me the value of typed functional programming has been it's ability to encode business logic in to the type system and thus validate some of my logic at compile time.
I wonder, aren't there problematic edge cases that depend on the data? For example, if you are computing a plane from a given set of points, there may be an edge case where the points are collinear. Assuming you always need to produce a valid plane, this would require some numerical analysis and regularization. Or is the answer to define some new type system that captures the non-collinearity? If so, it seems like the costs of enforcing that could overwhelm things...
Isn't that what also makes it hard to use <i></i>without<i></i> relying on the mathematical laws? While the math part is hard, it is what provides the strong guarantees in the form of laws.
The goal of software development should be about reducing the number of edge cases to the absolute minimum. If a tool makes it easier to keep track of all edge cases, it is bound to encourage developers to write code which contains more edge cases. But fundamentally, it still reduces code quality. Code with more edge cases is less flexible and not good at handling changing requirements.<p>I've seen this over and over in my career; tools can make people lazy in certain areas, it can put them on auto-pilot. Often, this can be a bad thing.<p>I like coding with dynamic languages because they get out of my way and shift the full responsibility for correctness on me. This creates a certain mental tension which helps me to perform. Keeps me alert and mindful.<p>I particularly try to avoid languages that make me wait for code to compile (which is more common with statically typed languages); this is for the same reason why don't like it when someone distracts me while I'm "in the zone" while coding.