3 ms·
What I think is most interesting is that the thing that enables F# code to be so readable is exactly the thing that I thought would be its biggest readability p
by bunderbunder 8y ago
What I think is most interesting is that the thing that enables F# code to be so readable is exactly the thing that I thought would be its biggest readability problem when I first encountered it: The function call syntax and semantics. Specifically, the fact that there's no requirement to place any brackets around an invocation or its argument list, and the currying by default.
But the same syntax is what makes partial application work so well, which is in turn what makes the pipeline operator (|>) work so well.
Also, the syntax does a good job of encouraging you to organize code for better readability. It's a lot like lisp's function call syntax, only it somehow makes me feel incentivized to capture intermediate values in let statements instead of nesting my function calls. I'm guessing it's because there's something vaguely pleasing about getting to skip the parens.
I also like that F#'s version of do blocks requires you to explicitly state what kind of computation expression you're using.
- dvlsg 8y agoI agree. The first time I saw F#, I was immediately concerned about single spaces without any braces/brackets/parens indicating function executions. I stuck it out with F#, though, and now I miss that syntax greatly when I have to use other languages. It's surprisingly wonderful (at least in my opinion). Or at least in languages where partial application / currying / composition is easily-available. I suspect that syntax wouldn't be very helpful with languages where that isn't the case.
- tombert 8y agoI actually completely disagree about the explicit `do` blocks; as a consequence of that decision, you can't write functions that abstract over all monads, at least not easily. I like F#, but that has always been my main gripe for the two years that I did F# full-time. I realize that for business code that that isn't a super big deal, but it becomes an issue if you want to write generic libraries; the closest thing that you can do is to exploit static-resolved types with member constraints and do explicit binds. For example, if I wanted to have a monad transformer for Async<Option>, I could define the custom monad, but then I end up having to wrap every regular Async function manually (or do a lot of manual unboxing) to make it work. A lot of the time, my logic in the async blocks isn't specific to async, and I feel that when you're forced to write custom unboxing everywhere, that's a good way to make a lot of mistakes. EDIT: Just an FYI, despite this complaint, I do really like F#...I find it an incredibly practical language, and one of the best of the Hindley-Milner-style functional languages.
- bunderbunder 8y agoIt's been a while, but, IIRC, the issue there is that computation expressions in F# are more a way of binding functionality to the syntax in an explicit way, than a direct expression of monads (or applicative functors, or whatever). F# doesn't even have a monad type to abstract over in its standard library in the first place. So what you might want to do instead is define your own library for working with abstractions, and then supply a computation expression or two for binding the syntax to the types defined in that library. But yeah, you'd still be stuck writing wrappers to mate any pre-existing types to the library. I'm not sure that's really even computation expressions' fault, so much as an inevitable implication of F#'s lack of any facility for ad-hoc polymorphism.
- tombert 8y agoWhile I agree that that's probably true behind the scenes, to define a new computation expression you still have to define the haskell-style "bind" and "return" functions, at least giving the impression to new F# engineers that it has some kind of generic monad type.
- phillipcarter 8y agoWe use the asyncMaybe CE extensively in the Visual Studio tooling for F# and although it's a little bit of boilerplate, in practice we don't need to lift an Async<'t> into an Async<'T option> very often. The worst example of this is probably this file: https://github.com/Microsoft/visualfsharp/blob/8ea71dd368401683fcbe895965c448c2688d3899/vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs#L52 https://github.com/Microsoft/visualfsharp/blob/8ea71dd368401... In most cases the APIs are uniform enough to where it's not a pain point.
- tombert 8y agoMy complaints are actually in the opposite direction, though I realize that I didn't explain this very well; I wanted to be able to use an Async<'T option> in already-existing async functions that weren't doing anything specific to async. A lot of my async stuff was just doing let! to "unbox" and returning at the end. In Haskell land I would have used something like (Monad m) => m a -> m b, and then I could throw any monad transformer into there.