Live data from Hacker News

Maybe Functions

blog.benwinding.com

71–80 of 103 posts

Re: Maybe Functions

#71
One thing I like about typescript is that unless you’re a masochist, it basically pushes you towards option 1.

If you have to constantly check for null/undefined it gets annoying and you naturally think about narrowing the state space so entire sections of your program don’t have to think about those possible states.

It should also become obvious when you have a possible null/undefined state and it’s super unclear what that piece of your program ought to do about it other than alert the parent (such as throwing an error). If a component doesn’t have a role to play when null, maybe it shouldn’t ever be seeing null as a possible state.

Re: Maybe Functions

#72
post #63

This looks too easy, the first solution. If there is no logged on user, which User object is fetchUser going to return? Which friends? At the top level, if I were to forget to check if someone is logged in, who knows what would happen here. I've worked on codebases where people were so allergic to the "billion dollar mistake" of nulls, that they created empty objects to return instead of returning null. This bit us i…

I’ve had limited success with the null object pattern but there is one case that it worked really well for me. I worked on a feature that was highly dynamic and users could compose reports selecting data points from tangentially related models. Null objects were a really helpful pattern because it was hard to anticipate how models would be composed and if a developer made a mistake it was hard to notice there was no effect. Our null objects would raise exceptions in development and explain what you need to change but wouldn’t prevent execution in production.

You could easily argue we should have just presented this exception to the user in all cases but this is where we landed. It’s probably the only case this pattern was beneficial for me.

Re: Maybe Functions

#73
post #37

I would argue that the vast majority of functions in real world software are maybe functions in that they can fail. You need to be able to deal with failure. Not only can the user not be logged in, there can be a network issue, etc that makes even downstream functions fail. Also, you have to deal with developer mistakes and what happens when they call incorrectly. This can be something as simple as getting the first…

> Also, you have to deal with developer mistakes and what happens when they call incorrectly. There is only one safe(ish) way to deal with programmer errors: crash. Hopefully loudly and early enough so it gets discovered in testing.

I assume you don't write device drivers or operating systems?

Predicting every possible failure reason for a function is impossible. Every function is a maybe function.

Re: Maybe Functions

#74
Respectfully, I don't think this articles uses monads correctly, because it's not using any. This could be very elegant:

  getUser: Option[User]
  getFriends(u: User): Seq[Friend]
  bestFriends(f: Seq[Friend]): Seq[Friend]
  renderFriends(f: Seq[Friend]): Option[UI] // Unit or type UI or HTML or ...

Only `getUser` actually returns an option and is explicit about it. `renderFriends` could arguably do without.

To call, we can do

  bestFriends: Option[Seq[Friends]] = getUser.flatMap(u: User => renderFriends(bestFriends(getFriends(u))))
The render function could either gracefully render an empty list or error check as part of the `flatMap`, which takes the form of

  flatMap[B](f: A => Option[B]): Option[B]
I really, really dislike it when functions signatures are lying to me, since `User` is clearly != `Option[User]` and `null` will not fit the type semantics of `User`, whatever those are.

And if you don't _call_ it mondads (but rather something more approachable), it's not that wild and scary sounding a concept all of a sudden.

That way, your compiler error checks null-type scenarios for you, your type signatures are clean, don't lie, and your compiler forces you to explicitly do something like `runSafely` (or `runUnsafe` etc.), usually a single point of failure.

Bonus, `MonadError`-type constructs are awesome too, since I get

  handleErrorWith[A](fa: F[A])(f: E => F[A]): F[A]
type functions (this is from cats in scala) to deal with errors explicitly.

Re: Maybe Functions

#76
I got stuck at option 1. Rendering code becomes lot more complex. Also few errors that make it difficult to follow the essay, like "it’s a “maybe” function as it only returns the friends of a user" but the function is getUser, not getFriends.

Or function getFriends(user: User): Friend[] { return fetchUser(); } The body of the function is wrong.

Re: Maybe Functions

#77
I know it's a simple example, but the Maybe class should probably use a null check internally rather than a truthy check, so that types like number and string which have falsy values that are nonetheless valid for a use case can be used with Maybe.

Re: Maybe Functions

#78
post #8

Agreed with this essay, and I think it rhymes with two others that I've found pretty influential over the past five years: 1. Parse, don't validate ( https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va... ) 2. Pipeline-oriented programming ( https://fsharpforfunandprofit.com/pipeline/ ) In my experience, the "best" code (defining "best" as some abstract melange of "easy to reason about", "easy to modify", "e…

I hold these two essays similarly high in influence for myself. The pipeline/railway oriented programming really made it click about how to use first-class types to deal with error cases elegantly.

Unfortunately, a lot of languages make it difficult to have the compiler enforce exhaustiveness.

Re: Maybe Functions

#79

Respectfully, I don't think this articles uses monads correctly, because it's not using any. This could be very elegant: getUser: Option[User] getFriends(u: User): Seq[Friend] bestFriends(f: Seq[Friend]): Seq[Friend] renderFriends(f: Seq[Friend]): Option[UI] // Unit or type UI or HTML or ... Only `getUser` actually returns an option and is explicit about it. `renderFriends` could arguably do without. To call, we can…

If anything the real lesson here is that you shouldn't try to lift your functions manually in the presence of a Monad. Monads tend to be somewhat 'infectious' in that anything that touches the Monad will need to be monadic. It's the reason why 'nullable' and 'async' can end up transforming most of the code-base to support their use.

And if you are going to write a sum type do it properly. If the language doesn't provide sum types but does have function types just use the category theory definition:

    function maybeWithUser(withUser: User => T, default: () => T): T {
        if (!loggedIn) return default()
        return withUser(fetchUser());
    }
Wrap this in a class if you really want to, but the idea is the same. This then results in pretty much the code he lists in example 1, exactly because most of the functions are just regular functions:

    function Page() {
      const bestFriends = maybeWithUser(user => {
        const friends = getFriends(user);
        const bestFriends = filterBestFriends(friends);
        return render(bestFriends);
      }, null);
      return 
        {bestFriends}
      ;
    }
Of course sometimes it's better to just use what you have rather than try to use language features that aren't quite there. It helps if you can recognise what's going on though.

Re: Maybe Functions

#80
I'm surprised the option (pun intended) that immediately came to my mind was not discussed: change the getUser function so it has a "LoggedInUser" parameter, instead of pulling the User from some global state. Then (so long as you have a type system) you can't call the function without the user being logged in.
Post reply on HN