Hacker News new | ask | show | jobs
by duijf 2138 days ago
My favorite real world example of how you can use these techniques to reduce bugs:

    -- Bad.
    data Session = Session
      { authenticated :: Bool
      , challenge :: Maybe Challenge
      , userId :: Maybe UserId
      }

    -- Good.
    data Session 
      = Unauthenticated
      | Authenticating Challenge
      | Authenticated UserId
(Credits to my friend Arian for the example [1])

The first type permits a value like `Session { authenticated = True, challenge = Challenge "<some challenge>", userId = UserId 1 }`. That's a nonsensical value which doesn't make sense in terms of the business logic.

Generally, when you have types which permit values like this:

- Someone (you or a coworker) will eventually write some code that constructs such nonsensical values.

- This means that you need a lot of tests to ensure that all code using this type works correctly when given such nonsensical values.

By using the second definition of `Session`, you don't have to worry about this at all. Nonsensical values can never exist so you will not accidentally construct them. Therefore you need less tests to ensure your code is correct.

- - -

For some reason, people are really keen to get into code like this when boolean flags are involved. Consider the following code (this time in Python):

    from dataclasses import dataclass

    @dataclass
    class Options:
        connect_tls: bool
        verify_cert: bool
        some_other_setting: int
It does not make sense to have `Options(connect_tls=False, verify_cert=True, ...)`. There is no certificate to validate when you connect without TLS.

(This generally happens when someone is tasked with implementing the `verify_cert` option. They see the existing options type, the existing flag for `connect_tls` and just add a second boolean.)

When I review code like this, I generally advocate for using Enums:

    from dataclasses import dataclass
    from enum import Enum, auto

    class ConnectionOptions(Enum):
        # Could also be: `PLAIN = 'plain'` or `PLAIN = 1`
        PLAIN = auto()
        TLS_UNVERIFIED = auto()
        TLS = auto()

    @dataclass
    class Options:
        connection_options: ConnectionOptions
        some_other_setting: int
It's almost the same safety level as in Haskell (although the Enum has a default serialization, which you need to think about e.g. when you store it in a database).

[1]: https://twitter.com/ProgrammerDude/status/124908893689234637...