Scala's Option monad versus null-conditional operator in C#
codewithstyle.info
codewithstyle.info
Or you can go the whole hog like I have my language-ext project [1][2].
In C# it would look like this:
var city = from author in article.author
from address in author.address
from city in address.city
select city.ToLower();
Which is arguably even more elegant than the Scala 'for' notation.As an aside, there was a discussion [3] on one of the language-ext issues about my Try<T> implementation, and I was asked to compare it to Scala's Try monad. The crux of it was that it's perfectly possible to capture the majority of the use-cases, but may be of interest in the context of this discussion.
[1] https://github.com/louthy/language-ext
[2] https://github.com/louthy/language-ext/blob/master/LanguageE...
[3] https://github.com/louthy/language-ext/issues/33#issuecommen...
Being able to abstract over monads (and other type constructors) ... that's the part that really matters.
C# is not expressive enough for that.
It is actually possible to get most of the way to HKT using extension methods. But it involves a hell of a lot of typing. In my project I used a template generator to do it. It's still pretty limited though, and there isn't a single type-class called Monad.
That's what I referred to.
> ...
The point is that you can't leverage all the existing monad/functor/applicative/free/yoneda... libraries out there–they don't exist, because they can't be written in a way that would allow re-use.
Agreed. I can only hope one day that the CLR team decide it's a reasonable thing to support so that C# (and F#) can both support it. It seems to be the blockage to the language teams (they won't move without it, even if they could implement it in a weaker/slower way on top of the existing type system). I've jumped through many hoops to get as much of a functional 'base class library' as possible in C# (I needed it for a very large C# project I maintain and want to bring more stability to), but language support would be preferred.
Monad Transformers for one. Traverse/Sequence are another super common (amazing) pattern that requires abstracting over types.
http://eed3si9n.com/learning-scalaz-day12
is a nice tutorial series on it.
So in F# I've been using specialized asyncSeq and asyncMaybe libraries. With monad transformers (and thus HKT's), would those come largely for free?
And you could traverse a list<maybe<list<_>> combo like [Some [Some [1; 2]; None; Some [3]]; None; Some [None; Some [4]]] with no more difficulty than a regular list-of-lists?
You can also write a generic "sequence" over a list, for instance, so that any 'monad' (task/future, maybe/option, state, either/disjunction etc.) can 'swap places'.
So your List<Future<A>> can become Future<List<A>>. But you only need to write it once. Then it doesn't matter what the inner type of the List is, as long as it's a monad (applicative, really, but all monads are applicatives).
Now looking at the Scalaz impl, the transformers are actually the core thing, and monads themselves are just transformers over Identity. So it all fits and isn't redundant. Certainly changes a lot of perspective.
[1] https://github.com/louthy/language-ext/blob/master/LanguageE...
But that still doesn't change the crux of the blog post, it's disingenuous comparing the Option monad to a null coalescing operator.
Ruby 2.3 &. operator and Swift can do something like this pseudocode:
if let city = article?.author?.address?.city?.ToLower() {
// city present
}
Too long?: if let city = article?
.author?
.address?
.city?
.ToLower() {
// city present
}
All are improvements over pyramids of doom.(Be aware of subtle gotcha in Scala of var and val, really prefer Rust/Go/Swift keywords.)
First, on the scala standard library and monads: Scala very carefully avoided creating a Monad trait, on purpose. So while List and Options are monads, we can't just go and say that everything that has a flatmap is a monad.
Also, the reason we can combine options and lists in a for comprehension is not really that they both have a flatmap: Try doing that with Future instead, and it will not work quite so easily! The reason those two have such easy interoperability is that in Scala's Predef there is a conversion that turns an Option into a Traversable, which is being called under the covers. For comprehensions are very powerful, but switching from one monadic context to another without help isn't one of them. In the case of Future and List, for instance, we'd have to use some of the utility methods in the Future object that can help transform a future into a List[Future], and a List[Future] into Future[List]
Don't get me wrong, it's great to see people trying to explain this kind of stuff, but it is so very hard to cover the topic in a way that is both approachable and that doesn't leave people with intuitions that will fail them outside of the given example.
I agree on the first point. As to the second, I didn't mean that having flatmap is the reason why we can combine Options and Lists. I meant that Options are composable with other Options and Lists are composable with other Lists.
I understand the problem of working with different monads. I looked into monad transformers at some point and found them difficult to work with so my approach now is to avoid mixing monad types (for example instead of having Future[Option[T]], just use a failed Future instead of None).
type Address = { street : string; city : string option }
type Author = { name : string; email : string; address : Address option }
type Article = { title : string; content : string; author : Author option }
let printCity (article : Article) =
article.author
|> Option.bind(fun author -> author.address)
|> Option.bind(fun address -> address.city)
|> Option.iter(fun city -> printfn "%s" city) maybe {
let! address = author.address
let! city = address.city
printfn %s city
}article.author.flatMap(_.address).flatMap(_.city).map(_.toLowerCase).foreach(println)
And in for-comprehension version there is no need to use yield, as we don't need result of println.
Wouldn't fit into the map type signature otherwise.
i.e. you should avoid:
serviceX.getServiceY().getServiceZ().getSomeValue();
But for just structured data it's not constructive to write accessor wrappers at each level. And for things like recursive data structures, it's not even possible.
Option/Maybe can represent arbitrary optionality, while null is limited to a sentinel value.
I wrote a blog post about null (https://www.lucidchart.com/techblog/2015/08/31/the-worst-mis...) and this one of the examples:
Suppose you have a generic key-value store. If a value is not present, the lookup returns null. You use it for a cache that maps user IDs to phone numbers. Some people don't have phone numbers, i.e. their phone number is null. But now you can't represent that in your cache. (JS "fixes" this -- if you want to call it that -- by using a second type of null: undefined.)
Option is generic and can applied arbitrarily deep; null/Nullable is a sentinel that can be used once.
Option is arbitrarily composable, and the superior choice.
You can have Option in C# and use it with linq to get more or less the same feature and syntax. There are a few implementations on nuget if you don't want to write your own.
(println (some-> article :author :address :city upper-case))So that you don't print nil if some-> returns nil.
Cheers.
It can be done in F# [1], but it's not pretty, and absolutely nails the compiler. The fact that the compiler can be cheated into doing it makes it even more frustrating that there isn't some official support.
Genuinely curious, but what are you doing that the lack of type classes hurts productivity so much?
I now work for one of the largest scala teams in the US. We make heavy use of the Scalaz project and regularly use monad transormers or free monads and both require higher kinded types.
I know that's by far not as good looking as the c# version, however at the end all comes down to syntactic sugar.
Console.WriteLine(article?.Author?.Address?.City?.ToUpper());
That is actually a bit different than the first piece of code, since it calls Console.WriteLine.Another reason to set tab stops to 4+ characters: make PoDs impractical and better code legible.