31 ms·
The general value of typed functional programming lies in leaving no edge cases
- melling 7y agoEdge case removed: for i=0 to 10 {print(a[i])} vs for x in a {print(x)} With functional programming it gets even better: a.filter {}.map{print} Pseudo code, of course. I list several “functional“ examples in Swift here: https://github.com/melling/SwiftCookBook/blob/master/functional.md https://github.com/melling/SwiftCookBook/blob/master/functio... Functional programming is also a higher level language. map, reduce,filter, flatten, flapMap, drop, take, zip, ... Learn the concepts in one language and they can be used in another functional language.
- Allower 7y agoLast I checked functional programming doesn't have a monopoly on "map, reduce,filter, flatten, flapMap, drop, take, zip", these are functions and can be implemented in any language, object oriented or otherwise.
- dleslie 7y agoThe problem in my domain, games, is the frequent boxing and unboxing of values when using functional iterator interfaces in languages common to the industry, particulalry C#. It just thrashes the heap and forces often too-frequent gc.
- AndrewDucker 7y agoI was under the impression that the way C# did generics meant that there was no need to box values. (Unlike Java, which treats all values as objects, even value types.)
- jayd16 7y agoC# has value types and many things are value types but certain things are not. Core library interfaces are implemented as with Object enumerator types for example. This causes IList.GetEnumerator() to return a boxed type. Its smart enough to use the value type when it knows it can though, such as List.GetEnumerator(). Lambdas have some allocations. I like LINQ but it can have some allocation as well. These are all tiny allocations but in a game you'd like to strive for 0 allocations.
- hinkley 7y agoTwo ways to interpret boxing here: 1 Storing a primitive as a first class object 2 packaging and repackaging data There may be situations where the former is not an issue, but the latter often is. If you don’t have a way to treat filter() as a generator/coroutine it means your traversing one collection and inserting those values into another, only to pass it to map() which iterates again and creates a third. If you’re doing that once a frame, maybe not a big deal. Three times per object per frame? That’s a lot of temporary data structures. If the other responder is right, I speculate that the borrow checker forced someone’s hand in Rust and they found that a generator keeps the borrowed objects on the call stack where they already had tools to track ownership.
- pjc50 7y agoCorrect usage of IEnumerable helps a lot, as well as the magic that Linq can do with expressions.
- dleslie 7y agoIn my experience, the trick is to remove `using Linq` from the entire project.
- hinkley 7y agoAs someone who has had a mild case of Linq jealousy for some time, I’m curious what makes you averse.
- dleslie 7y agoIt's too easy to fall into the temporary collection trap you described; on a large project with many developers, allowing Linq is a bit like leaving a toaster plugged in beside the pool.
- miloignis 7y agoWhile it may or may not be optimal for you/your field for other reasons, I believe Rust does iterators in a 0-cost, no allocation way and generally generates code equivalent to or better than a hand-written for loop.
- dleslie 7y agoThere are studios moving to rust, and there definitely is a great amount of interest in rust among my peers because of the 0-cost iterator features. The borrow-checking isn't even a big draw, it's the other features!
- Mirioron 7y agoWhile it does remove edge cases, the code also becomes more opaque. At least to me, functional style programming seems much harder to read and then reason about.
- dpc_pw 7y agoIt takes some time to get used to and change mental model from "program describes step by step what to do" to "program describes logical flow of information". I moved from first camp to the second one somewhere during my adult life. And IMO: Both are valid, sometimes first one is necessary (because that's how the machines actually work), but second one is clearly better most of the time for humans, especially in business-logic heavy cases.
- thaumasiotes 7y agoThere's an analogous switch in algebra from notation like f(g(x)) to something more like X_{GF} (where the _{} indicates subscripting, not written symbols). Note the better left-to-right ordering in the terser system. It's the same dichotomy; are you thinking of f(x) as an instruction to do f to x, or are you thinking of X_F as what happens when X goes through F?
- vippy 7y agoThis is a learning thing, honestly. Once you start thinking in types, it clicks: you're thinking at a higher level of abstraction, so the code is less important than understanding how the programmer put the types together in their head to solve a problem.
- Mirioron 7y agoBut I am already thinking about what the code should do. The problem is that if I read functional code I get bogged down in the details of what limits are set by the operations, because they're usually not as explicit. It's like reading a chain of functions on some element (actually, it quite literally is that).
- lmkg 7y ago
- zyxzevn 7y agoA lot of those concepts were already in Smalltalk. a do: [:x| Transcript print: x ] [] = closure That is, because Smaltalk was based on Lisp. But the other OO languages removed this concept, but it came back in Scala.
- thewebcount 7y agoI program mainly in C++, and I often start off writing things as: for (auto x : someContainer) { //do something with x } But inevitably, I end up needing either an iterator pointing to a particular item at the end, or I need an index to pass to some other function, and this style makes that impossible. Perhaps it's just the domain I work in, but even after years of trying to make this work, it still only works about half the time for what I'm doing.
- eru 7y agoIn Haskell map someFunction listOfValue if you need an index: map someOtherFunction (zip [0..] listOfValues) Python uses 'enumerate' to similar effect. Not sure if C++ has an equivalent?
- a1369209993 7y agoYou can also use: zipWith someOtherFunction [0..] listOfValues # , since someOtherFunction is likely to be # :: (Int -> Value -> Stuff) rather than # :: ((Int,Value) -> Stuff) . zipIndex f = zipWith f [0..] # is also often useful.
- eru 7y agoYes. I wanted to keep things simpler for people who don't speak Haskell; and show off composition of simpler combinators to build more exciting things at the same time. In eg OCaml you have to use something like zipIndex (but implemented differently) or mapWithIndex, because they don't do lazy data structures by default. (And in eg Python their lazy sequences come with lots of caveats because they are not referentially transparent.)
- hellofunk 7y agoWhy not just pass the reference instead of an iterator or index? That’s what I do the best majority of the time. It’s rare that I actually need a specific iterator or pointer. Just change your loop to auto& x.
- 7y ago
- cryptica 7y agoThe 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. 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. 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. 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.
- finolex1 7y agoIsn't this akin to saying I don't like wearing seatbelts because it makes me more complacent in my driving?
- cryptica 7y agoIt could actually be the case that seat-belts make drivers more complacent but the key difference in the case of coding is that you won't die if you make a typo in your code. If designed correctly, your tests will catch it anyway.
- Verdex 7y agoTherac-25 [1] disagrees with your statement. Although to be fair, the programmer didn't die. The patients did. [1] - https://en.wikipedia.org/wiki/Therac-25 https://en.wikipedia.org/wiki/Therac-25
- cjfd 7y agoYour tests? You should despise tests if you want to be consistent with the attitude described in your first post. In the mean time I will just keep using my strongly typed language and, of course, also automated tests. Thank you very much.
- nicoburns 7y agoForget 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). 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.
- jayd16 7y agoYou can use a functional style in those languages as well. Or do you mean with Rust's borrow checker you have less mutability to worry about?
- seren 7y agoIf you have a Rust enum with Color{Red,Green,Blue), if for some reason you update to Color{Red,Green,Blue,Yellow}, the rustc compiler will force you to manage the new Yellow case wherever you want to "match" a pattern on Color, so overall you have less chance to forget to manage the new case everywhere. This kind of error is not managed by a C or C++ compiler (as far as I know). But from my understanding, this is not directly related to the borrow checker, which tracks who owns a reference and is allowed to modify it or not. (That being said if you had a "default" case earlier, the compiler won't complain)
- ZephyrP 7y agoIf you compile with `-Wall` GCC will emit a warning for failing to exhaustively check enum values inside a switch statement (I don't remember the specific underlying feature flag name, sorry). EDIT: It's called `-Wswitch`
- seren 7y agoYes, apparently, there is a -Wswitch statement in gcc, which looks quite similar, maybe I have used too old compilers... > Warn whenever a switch statement has an index of enumerated type and lacks a case for one or more of the named codes of that enumeration. (The presence of a default label prevents this warning.) case labels outside the enumeration range also provoke warnings when this option is used (even if there is a default label). This warning is enabled by -Wall.
- jackfoxy 7y agoMost languages rely on coding patterns for code safety. This is merely a best practice, usually involves writing multiple lines of code, and is merely a convention. 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 convention or the compiler? 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.
- threatofrain 7y agoWhat kind of mathematical correctness are you getting by using FP? The same kinds that the Rust compiler offers?
- Filligree 7y agoDifferent, but related. If you call 'map' then you know you're not mutating the values, and you get an output the same size as the input without dropping values, and... It means you operate on a higher level of abstraction, and that inherently makes reading code faster. Not per line of code, but certainly per functional unit. Idiomatic Rust tends to use a lot of functional code, and provides all the tools to make that safe. Its map function could mutate the input -- it would be unidiomatic, but certainly possible -- except that, if you don't pass a mutable reference, then you're safe to assume it won't.
- yachtman 7y agoI 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).
- psnively 7y agoThat's unbelievably kind of you. Hopefully our paths will cross again!
- saityi 7y agoPaul 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: https://www.youtube.com/watch?v=8_HsFrXhZlA https://www.youtube.com/watch?v=8_HsFrXhZlA > 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!
- deleted 7y ago[deleted]
- mac01021 7y agoInteresting talk, but does that retry logic really provide me with more confidence about its correctness than equivalent impure code?
- psnively 7y agoThanks, and good question. I deliberately didn't go into the specifics about the various typeclasses at play and their laws, and how those are tested. Part of that is just for reasons of time, but the more salient reason is that I wanted to show how those of us who do purely functional programming do it in practice. So if you watched the presentation, you saw that most of my thinking was about "OK, how do I elaborate from the most trivial transformation (do nothing at all) to the one I really want?" And that proceeds compositionally: I transform this value to that value. OK, does that value have the right type? No? What do I need to do to ensure that it does? And the transformation steps have some important properties, like their scope being entirely local. I really tried to emphasize this at the end with `attemptRepeatedly`: my description of each line of the whopping three lines is exhaustive. When I say there's no point in writing a test for it, I mean that literally. There certainly are typeclasses at play, and I briefly talk about the `Catchable` typeclass, and show the ScalaDoc for it, which documents an important law: the relationship between catching ambient exceptions and the algebra provided by `Catchable`. I rely on the other typeclasses and other laws in a similar fashion. The other thing I think is pretty important is the part where I say "Let's look at cases," because the point there is that I can reason about the code by reasoning about the shape of the data it's manipulating. So if a `step` is an `attempt` of `p` that, if successful, `kill`s the retry `schedule` or, if unsuccessful, logs the exception, I'm still dealing with a `Process` of one element (by assumption that `p` will emit one element). Then `retries` will be a `Process` of 0 to infinity elements, because we put no constraints on `schedule`. So `(step ++ retries)` will be one or more elements, and because `step` `kill`s `schedule` on success, because `retries` is derived from `schedule`, `retries` is also `kill`ed. So `(step ++ retries)` is a `Process` that will emit one or more elements, with the last element being the first successful one, or the last failed element if none of the `retries` succeeds, so we take `last`. Then we just `fold` the failure or value back into a single effect, and we're done. As I discuss in the presentation, there certainly are questions. What happens if the `schedule` is empty? What happens if the `schedule` is infinite? An attendee in Q & A asked a really good question: was I sure `retries` would wait before the first retry, or was the semantics "try, then wait?" (It really is the former, but that wasn't clear from my recorded REPL session.) Of course, this isn't very impressive for a three-line example, although I think it's pretty striking that it only takes three lines, each of which can be completely reasoned about independently, to achieve a pretty significant operational goal. The point, though, is we can build entire systems this way, and in fact `attemptRepeatedly` is part of a distributed monitoring system I worked on at Intel Media/Verizon Labs, which is written entirely in this way apart from the monitoring types themselves, which present an imperative API for familiarity's sake (and which we later came to regret). I hope this helps!
- hristov 7y agoI am not sure this is the only value, but it is generally true. And this is one of the great advantages of haskell. 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. 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. 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.
- justAnotherNET 7y agoSwift has this as well, but calls them “Optional”. The concept is definitely useful!
- davinic 7y agoAlso Java has Optional<T> and C# has Nullable<T>.
- 7y ago
- adgasf 7y agoConversely, it's also one of the biggest pain-points when throwing together a quick prototype.
- worldsayshi 7y agoThen here's where typescript shines. To be able to move fluidly between no static checks of JavaScript and strong-ish type safety of typescript without too much work. Then again, it's not always without hiccups.
- jefflombardjr 7y agoThat is where I think Flow really shines. Seems to be less hiccups, but that is just my anecdotal experience.
- navaati 7y agoNah, you can totally use things like holes or undefined (in haskell), unimplemented!() in rust, etc. EDIT: and, as neighbour says, any in typescript ! But typescript, with its progressive typing, is in a league of its own.
- ken 7y agoThey 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. 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. 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 static functional language I know of is any better.
- JoelMcCracken 7y agoHaskell is so much better, and you would learn that with about 5 minutes learning about the number types
- eru 7y agoHaskell still doesn't track NaN in the type system.
- JoelMcCracken 7y agoYou're right, it does, and it exists at the value level if you are using floating point. But in Haskell you have, for example, Rational numbers to avoid many such problems. I was, however, surprised to not see any kind of a (significant) `safeDiv` function on hackage, and no NonZero newtype (outside of quickcheck). But, for this kind of thing its so easy to roll yourself, I am guessing this is what people do (and I have seen).
- ska 7y ago> But in Haskell you have, for example, Rational numbers to avoid many such problems. Hey, it's for sure nice to have a rich numeric stack (see also some lisps), but in numeric work "use something else" is very rarely a practical answer to issues with IEEE-754.
- phibz 7y agoFor 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.
- Naac 7y agoWhat's specifically functional about this advantage. Can't you encode the buisness logic into types in non-functional languages as well?
- eru 7y agoThe type systems that are common in functional languages are usually more flexible and useful than what you find in most other common languages (in particular, usually more useful than the type systems of common object oriented languages).
- hellofunk 7y agoThe word “type“ has a fairly different meaning in functional programming compared to object oriented programming. At a very shallow level, they have some similarities. But the way they are used is vastly different between the two paradigms.
- ativzzz 7y agoYou can encode it, but it is not enforced. For instance, I can refer to every instance/argument when I use a Person object as `person` or whatever name of the class is, but when you have many developers working on a large system, these things tend to slowly spiral unless you establish strict guidelines early on and enforce through code reviews. Or you can use types where it's enforced by the compiler.
- marcosdumay 7y agoOn practice, no you can't. At least today. But it may be a very interesting question for a researcher.
- pepper_sauce 7y agoSomething about putting business logic in a type system irks me more than someone building another compiler or database in Haskell. Business logic relies so much on run-time values that (IMO) using data and predicates is a better fit.
- proc0 7y agoIsn't that what also makes it hard to use without relying on the mathematical laws? While the math part is hard, it is what provides the strong guarantees in the form of laws.
- agentultra 7y agoI 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. head :: [a] -> a -- versus saferHead :: [a] -> Maybe a -- versus safeHead :: NonEmpty a -> a The first function is partial because we'll get an exception if we give it the empty list. 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). The last one forces the responsibility on the caller to provide a non-empty input. 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. 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. Update Added a trivial example to demonstrate "partial," etc.
- BoiledCabbage 7y ago100% that's a big part of it (although there are others). Although if someone doesn't know haskell, then the above notation for functions getting the first element of a list is probably not of much value to them. Clearer would be: char head(List<char> x); char? saferHead(List<char> x); char safeHead(NonEmptyList<char> x);
- lgas 7y agoThere's also another option in the middle which is a version that takes a value to return when the list is empty headOr :: a -> [a] -> a which is similar to the Maybe solution but forces the caller to discharge the Maybe immediately rather than letting them potentially clutter up the rest of their code by proliferating the Maybe.
- thaw13579 7y agoI 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...
- tolmasky 7y agoThe answer doesn’t necessarily have to be statically known, just ensure that the edge case is handled. computePlane() can return Maybe<Plane>, where Nothing is returned in the colinear case. This seems not that different from throwing an exception, except the caller can’t accidentally forget to deal with the colinear case, there must be code to handle the Nothing case. But perhaps the question you’re asking is “but how can the writer of toPlane() know THEY did the right thing”. There’s of course no solution to logic errors (function subtract(a,b) { return a + b } will get by most type systems, short of having the type system re-encode the function itself, at which point it’s just correctness through redundancy — unless both the type AND function are wrong!). However, you COULD protect against future people breaking your implementation by doing something analogous time weak_ptr’s implementation and flipping the Maybe to the input vs the output). So for example, you could make a type NonColinearSet, and have the “constructor” take a PointSet, but return a Maybe<NonColinearSet>, so asNonColinearSet takes a PointSet and returns Nothing if the PointSet is colinear, or NonColinearSet if it isn’t. Now you computePlane() function can take a NonColinearSet and return Plane (not Maybe<Plane>) since it “knows” the input must be valid, so it can ignore edge cases internally, at the cost of the caller having to deal with toNonColinearSet returning Nothing before calling it, since you won’t be allowed to transparent pass the result of asNonColinearSet(pointSet) to computePlane since the types Wong match (Maybe<NonColinearSet> vs. NonColinearSet). maybeNonColinear = asNonColinearSet(pointSet); case Nothing: alertUserBadDataOrWhatever(); case Just<NonColinearSet>: return toPlane(maybeNonColinear.value) Notice in both cases, ultimately the caller must do something in the edge case, which is what you want.
- fctorial 7y agoOTOH it's nice to be able to do: try { // 500 lines catch (e) { return 4; }
- Twisol 7y agoOften, even functional languages have something similar, whether it's call/cc or something more cutting-edge like algebraic effects. All of these essentially augment the execution environment in various interesting ways, and are enhanced by other features of functional languages like immutability. (More pithily: try/catch is not inherently non-functional.)
- mac01021 7y agoAs a mere mortal reading the code, I would rather not have to reason about try blocks longer than a couple of lines.