11 ms·
How it feels to join an all-Haskell startup
- saosebastiao 11y agoHow was that snippet an improvement to your coding style? The original was extremely clear, and now someone who reads your code has to understand what (,) <$> and <*> do. IMO this tendency for code golf one-upmanship is a huge detractor for anyone wanting to learn the language.
- msie 11y agoI agree. Could understand that first snippet, the second, not so much. I'm appreciating Javascript more and more because stuff like that doesn't happen there.
- jonnybgood 11y agoThis is common complaint I see of the Haskell community where there is always an attempt at cleverness over clarity.
- ricccardo 11y agoUnfortunately I think it's the language, not the community. It definitely happenes with my own code as well, no hints from external developers are required :-) I miss Python in this regard.
- rectangletangle 11y agoKind of ironic seeing as Python takes quite a few things from Haskell, like comprehensions, and zip.
- danidiaz 11y agoThe use of Applicative is actually an attempt at clarity, by restricting the kinds of things that the code can actually do. As it name implies, Applicative is a kind of "effectful application". There have been proposals to implement a more natural syntax that makes the connection more clear.
- spopejoy 11y agoThey're about equivalent IMO, which yes, means golf. However, I don't see what's so terrible about learning 3 new functions. Sorry it doesn't look like Javascript anymore.
- jonnybgood 11y agoI don't think the issue is learning 3 new functions. The author appears to be an experienced Haskeller, so I'm sure they're aware of applicative functors. I think the issue is whether the usage of applicative functors was necessary or just for the sake of being clever for no added benefit.
- cousin_it 11y agoI feel that it's important to have code that is immediately readable and understandable, without requiring much context. Smalltalkers had it right: > If a system is to serve the creative spirit, it must be entirely comprehensible to a single individual. Imagine you're reading a musical score. Suddenly there's an unfamiliar symbol. Oh right, you need to turn to page 5 to figure out how it's defined. Oh no, it's defined in terms of more symbols. There goes your flow. The tools that enable the most creativity are the tools that you can hold in your head completely, while you focus on the task at hand. C is a tool like that, regardless of its other drawbacks. Unfortunately, Haskell isn't quite like that, due to its culture of extracting every little thing and giving it a unique name. Now you need to memorize all these names, instead of e.g. understanding the concept of a "for loop" once and applying it forever.
- Dewie3 11y agoI think this is a bad comparison. If you're, say, reading music in order to play it, why would you need things like functions and renamings in order to read it? Just read it straight off - one bar at the time. There isn't any need to have some kind of "pattern" or "abstraction" that compresses 20 bars into "this one thing, more or less". And you would have to read all of it to play the piece faithfully anyway. Maybe there is some utility to be able to say "I don't care about the nuances; just show me the general structure right now". I don't know, I'm not a composer. On the other hand, it is of course tremendously useful in programming to look at some lines of code and be able to say "Oh, so this code this and that", then moving on. If you want more details, dive into that section more. You soar over the code to get an overview and dive down for more specifics when you need to. A music piece can be read (and played) from beginning to end, but that is less useful in programming. Maybe for a late evening with a bottle of wine when you want to appreciate the beauty of a code base that you really like, I guess.
- tel 11y agoI was about to try to talk about the genuine elegance of Applicatives—and I'm more than happy to defend them in general—but then, yeah. In this instance, I'm not sure I'd be able to argue that the change was such a good choice. liftIO (liftA2 (,) randomIO getCurrentTime) Eh, I'm not sure I could defend it. I'd honestly probably actually do bid <- liftIO randomIO now <- liftIO getCurrentTime
- dllthomas 11y agoOf course, there is always https://ghc.haskell.org/trac/ghc/wiki/ApplicativeDo https://ghc.haskell.org/trac/ghc/wiki/ApplicativeDo
- tel 11y agoIt's worth noting that there's a small potential performance penalty to this if it's being called all the time and it's being lifted through a large transformer stack.
- droque 11y agoThe change they made was from a monad to an applicative, which is less "hands on", thus preferred. Kind of preferring map over hand-written loops. (Although the golfing is definitively there and in the community, see pointfree/pointless style https://wiki.haskell.org/Pointfree https://wiki.haskell.org/Pointfree)
- le0n 11y agoI prefer comprehensions over either mapping or explicit recursion. Mapping is a win for abstraction but a loss for clarity.
- pash 11y agoIt's somewhat debatable whether the refactor is an improvement, but there is at least one good argument that it is, and (in my opinion) only a much poorer argument that it makes things less clear. If the applicative code is better, it's because applicatives are strictly less powerful than monads, in the sense that everything you can do with applicatives you can also do with monads, but not vice-versa. Many Haskellers prefer the abstraction that is "just powerful enough", or what's the same thing, they prefer the abstraction that is "least powerful"—that is, Haskellers try to write code in a way that lets them do what they want want to do, but at the same time in a way that doesn't allow them do things they didn't mean to do. And for just that reason: using the least powerful abstraction prevents you from introducing bugs by keeping you from writing some of the things that you didn't intend to write. Thus the least powerful abstraction is the best abstraction, in the sense that it's the "best fit" (and, yes, also in an aesthetic sense). So if you can write what you want using applicatives rather than monads, you should prefer applicatives, the argument goes. (And if you can write it with functors, prefer functors to applicatives.) For the same reason, many Haskellers prefer to use restrictions of the IO type that limit what you can do rather than IO itself, whenever it's convenient. Applicative style, in this case, also keeps you from having to name a couple of things, which means two fewer "hard things" (in Dijkstra's estimation) for the programmer to deal with—and just two fewer things, which means two fewer things to screw up, period. The counter-argument, that applicatives are somehow more complicated or less clear than monads, is a poor one, I think. Haskell is not a superficial language. It's not one you're meant to be able to read and understand just because you're a competent programmer in some other, unrelated language. If you want to program in Haskell, you need to understand the languange's core abstractions—and, yes, applicative style is one of them at this point. That does mean there's more to learn. But Haskell is all about giving programmers the ability to recognize and use the right abstraction; the time spent learning is worth your while. Further, if you're not a Haskell programmer, I think you should beware of pretending that you understand the monadic "before" code and therefore thinking that it's simpler than the applicative "after" code that you don't pretend to understand. First, because (syntactic subterfuge notwithstanding) you don't really understand the monadic code, unless you've learned the underlying concepts; and second, because it's not simpler. It's just not. There is more going on in the monadic code, not less. Now, if you're a beginner and you understand monads but not applicatives, you have an argument—the monadic code is familiar, and the applicative code isn't—but that just means you have more to learn. Go learn it. We're here to help.
- cherry_su 11y agoOf the three, I'd contend <$> is the most reasonable to do since it's very similar to $ (which is commonly used), but applied to a boxed argument (Functor).
- tel 11y agoOn the other hand, <*> is closer in spirit to what $ is.
- thrwwy5 11y agoThis to me is the biggest barrier to reading anyone else's Haskell code. It seems like it's always full of unfamiliar, ungooglable infix operators.
- dullcrisp 11y agoTry https://www.haskell.org/hoogle/ https://www.haskell.org/hoogle/.
- thrwwy5 11y agothanks
- wetmore 11y agoIf you are hired at a company exclusively doing Haskell there is no way you haven't seen <$>, <*>, or (,) before. In this case, the reason the OP didn't think of writing it with Applicative isn't because he didn't know about them (I assume), but just because he didn't think to use it in that particular case.
- jbergens 11y agoThat is one point of having code reviews, to get everyone in a team or an organization to write (mostly) the same type of code.
- thom 11y agoOne of our Haskellers once tried to explain the bizarre internal consistency of the many Haskell's lens operators. It's basically just trolling in API form: https://hackage.haskell.org/package/lens-3.8.5/docs/Control-Lens-Operators.html https://hackage.haskell.org/package/lens-3.8.5/docs/Control-...
- tel 11y agoIt's tragic people feel that way. They're actually very well-designed, but if you don't like operator soup then no amount of design will convince you otherwise. Which is fine and why the Lens package is able to be imported operator-free.
- Dewie3 11y agoThat refactor is of a common pattern (I've seen it before; I've probably written it myself) into a bog-standard use of Applicative code. This use is pretty much the introductory example for Applicative. Do you use Haskell? Because Applicative is fairly well-known, and the operators etc. aren't considered esoteric and they are not something you are likely to have to learn when you encounter them. If you don't use Haskell, on the other hand, I don't see how you are in a position to critique the readability of a refactor. The philosophy at play here is probably not code golf as much as it is about the principle of least power[1]. Applicative is strictly less powerful than Monad. So when reading Applicative code, there is less stuff to look out for -- it is great to be able to know what an expression can't do, when reading it. Maybe the do-notation makes it look prettier, but I don't know if it makes it more readable. Maybe more superficially look like imperative code which makes one think Oh, I get this. But it might be a false sense of safety. In any case, I guess Applicative do notation can be used (at some point). [1] http://en.wikipedia.org/wiki/Rule_of_least_power http://en.wikipedia.org/wiki/Rule_of_least_power
- bgar 11y ago(,) <$> and <*> are very common functions that every Haskeller knows... the revision is understandable if you write Haskell.
- m_mueller 11y agoCould someone explain 'applicative'? To me it seems like throwing some magic syntax at a simple usecase to make it more terse and less readable - but maybe I misunderstand and it's something every Haskell programmer knows.
- nbouscal 11y agoApplicatives are definitely something that every Haskell programmer knows. I would expect everyone on the team to understand the recommended new version, but it's less clear whether it is actually an improvement. There is a proposal to allow applicatives to use the same syntax as monads (the original code, before the change), which would render the question moot: https://ghc.haskell.org/trac/ghc/wiki/ApplicativeDo https://ghc.haskell.org/trac/ghc/wiki/ApplicativeDo
- purescript 11y agoAn Applicative functor is a type constructor which lets you lift functions of arbitrary numbers of arguments to functions whose arguments are wrapped with that type constructor. Concretely, if you think about promises in JavaScript, for example, if you have a function A -> B -> C -> D, say, and you have three promises of types Promise A, Promise B and Promise C, you can construct a Promise of type Promise D by running the three in parallel, and applying your three-argument function when they are all done. So you've turned a function of type A -> B -> C -> D into one of type Promise A -> Promise B -> Promise C -> Promise D. If you can do that sensibly for any number of function arguments, you have an Applicative. ("sensibly" here means that there are type class laws which have to hold) Every Monad is also an Applicative functor, since you could use do notation to compose your promises instead, but there are other interesting Applicatives which do not come from Monads.
- spopejoy 11y agoIn most cases (like the example), it simply allows you to call "effectful" code (often monadic, IO etc) to populate arguments of a function. My favorite example is "I want to grab three web pages concurrently and return them in a single triple." To the haters who think this is just golf, just TRY to write this as beautifully in any other language. (page1, page2, page3) <- runConcurrently $ (,,) <$> Concurrently (getURL "url1") <*> Concurrently (getURL "url2") <*> Concurrently (getURL "url3") (taken from the excellent haddocks for Control.Current.Async https://hackage.haskell.org/package/async-2.0.1.4/docs/Control-Concurrent-Async.html#t:Concurrently https://hackage.haskell.org/package/async-2.0.1.4/docs/Contr...)
- buckie 11y agoAs someone who also recently joined an all haskell team, but hadn't coded professionally in haskell before, I'll add the biggest thing I've noticed to the "how it feels" list: On a biweekly basis I'm shown something new causing my brain to melt, leak out of my ear & pool onto the floor near my desk. It's awesome[0]! Today's was using impredicative types + `$` has a special case for dealing with it[1]. [0]: ...some number of hours later when I've collected my thoughts and think "OHHH... duh" [1]: https://ghc.haskell.org/trac/ghc/wiki/ImpredicativePolymorphism https://ghc.haskell.org/trac/ghc/wiki/ImpredicativePolymorph...
- gohrt 11y agoLove it or hate it, those 2 screenshots are life of a Haskeller: 1. ever-more compact abstractions with awkward names 2. chasing down space leaks with janky gnuplot'd reports.
- deleted 11y ago[deleted]
- suls 11y agoAs much as I would love to use Haskell professionally, I always wonder if teams/projects using Haskell don't also struggle with all the sociology involved in developing software .. Sure, being able to use Applicative reduces lots of code duplication, brings in better clarity and all .. but what if the product owner still remains a jerk without a vision? In all the teams I have been so far, I can honestly say that the _language_ itself was never _the_ problem. It was always a combination of communication, skill or product vision. The only thing I can imagine is that by choosing Haskell you tend to get better skilled developers - so only communication and product vision remain that could ruin your project/product. Any thoughts?
- dons 11y agoHabitually rewriting things in applicative and/or pointfree is a sure sign of a junior Haskell dev. One who has never had to maintain someone else's code. There are cases where it helps (e.g. structured parsing), but all too often this is simply obfuscation. Even if motivation is good -- learning new structures -- you still have to focus on cost/benefit of each change in structure. Keep it simple, folks.
- chriswarbo 11y agoI'm generally in favour of introducing applicatives, but I don't think the code in the article is a good example: (bid, now) <- liftIO $ do b <- randomIO n <- getCurrentTime return (b, n) (bid, now) <- liftIO $ (,) <$> randomIO <*> getCurrentTime The applicative code could be considered better, since it avoids one-shot variables. However, that's not the biggest smell with this code. To me, the problems are: - Constructing-then-destructing a pair; is it really necessary? - Use of `$`; is there a way to avoid having to alter the precedence? - Combining two seemingly unrelated actions into one; `randomIO` should be completely uncorrelated to the current time, so why put them in the same action? - Potential laziness issues; it looks like we're using the `(bid, now)` combination to ensure the two IO actions are executed together, but will they be? WHNF will give us `(_ , _)`; if we force `bid`, will `now` also be forced? Not saying I have answers to these, but I would say those are more "smelly" than the do-notation with a return
- tome 11y agoCode review 4 lines: hundreds of comments Code review 1000 lines: "Looks fine"
- chriswarbo 11y agoWell, my main comment is that the most pungent smells in this code depend on its context and what it's trying to achieve. If I encountered it, I would immediately look at the surrounding context and usage to see if it's a reasonable approach to the problem. Without that context, all that can be done are superficial changes like monad/applicative; which in this case are very minor. In other words, the original code wasn't a bad approach to the problem; but is it solving the right problem?