22

In PHP (yes, it's a language I... don't hate) I've always hated exceptions. They're like GOTO, in an OOP world with interfaces and contracts, try/catch is really odd as it breaks a promise about returning with a typed value.

But you can now do this in PHP8, which comes pretty close to Maybe/Either monads (Option, Result whatever it's called in other languages):

function getUser(): User | UserNotFound

PHP8 unions don't come with the same strong guarantees as in other languages but *pets PHP gently on the head* you did well, my boy.

Now I would really love it if PHP9 could do:

function getUsers(): Collection<User>

Type Tree<T> = Null | Node<T>;
function 🎄(): Tree<Branch<Ornament|Light|null>>

Comments
  • 4
    This is exactly why I dislike exceptions in C#, too.
    Because they are abused for error propagation generally.

    C# is currently missing something like the Either type system which makes any non-Exception error propagation sub-optimal. So it's a tradeoff.

    Unfortunately, most devs just don't think about it at all and do always use Exceptions. Which grinds my gears.

    I mean Exceptions do have their uses but they are horrendously overused.
  • 3
    This is a fundamental problem in all languages but Swift has managed to solve this very nicely.

    You can return the Result type from a function which contains either a success value or a failure error.
    But if you want the flexibility of Exceptions, the call side can decide the failure case to be thrown as an error.

    This is amazing because it gives you the best of two worlds with essentially no tradeoffs.
  • 4
    @Lensflare Yeah modern languages like Swift and Rust rightfully adopted Maybe/Option/Either/Result from FP languages like Haskell/Scala.

    Then Go saw it, tried it, and fucked it up.
  • 2
    @bittersweet how did Go fuck it up? 😄
  • 3
    @Lensflare

    ADTs, algebraic data types, are simply functions one meta-level up. A function takes a string and an int, and returns an bool — while an ADT definition takes multiple types and return a new type.

    For example, you can have a Collection of Users, one single User, a Tree of Users, a Result of Either User or Error, a Queue of Users, a Pair of User and Cat, etc.

    Super useful for composing relations — because some functions might take any Iterable structure containing Users, while other functions might take Trees regardless of whether the nodes are Users or Pandas.

    The Option/Maybe type is basically a value which is either T (any type) OR null/nil/empty. The Result type is T OR Error, and the Either type tends to be defined as a wrapper around two arbitrary types.

    Go... Doesn't do ADTs, for simplicity reasons. So they defined their result type basically as:

    (User or nil) and (Error or nil)

    Which leads to pretty verbose code.

    At a local level it is easy to read (simple syntax), to understand individual parts of the code, because the language doesn't need any type system constructs (Trait/Typeclass information) and the error check is just "if err != Nil".

    But it makes code harder to organize & read at a higher level, because your code is littered with these checks.

    Also, because Go returns a pair of both Value and Error, you can ignore either, or have a function which returns both Value and Error, or neither. In Haskell, if you get a "Maybe User" it's really either User or Nothing, and to get to the User your code MUST specify what to do if there's Nothing.
  • 3
    @Lensflare

    In short, I think Go is a cute simple language which is unsuitable for most tasks except as a bash ops scripting replacement.

    It's redeeming feature is that it's the only simple language with a great parallelization paradigm (goroutines/channels), although I'd argue you might as well learn Rust+Tokio if you need to optimize for multicore & async to that extent.
  • 2
    Nice explanation. I always wondered what Algebraic Types are.

    So, Go is struggling with the same lack of features as C# in this regard.
    I know exactly what you mean when you are talking about the problem of having a result type which contains each possible outcome as a separate property. It gets only worse when you have more than 2 possible outcomes.

    Kotlin has sealed classes and Swift has enums with associated values for that.
    In fact, Result is implemented as such an enum.

    I wonder if this means that Kotlin and Swift have algebraic types or if there is more to it.
  • 2
    @Lensflare

    Yeah correct, unlike Java, Kotlin will actually refuse to compile unless you handle both options explicitly.

    https://medium.com/sharenowtech/...

    The nice part is that utilizing such type system features severely reduces the amount of accidental null pointer exceptions you'll get during runtime.
  • 2
    You don't need PHP8 for
    function getUser(): ?User

    But generic unions are a good thing for sure.
    I too hope for generics like array<string, User|UserGroup> (map of string to union of User or UserGroup).
  • 0
    @Oktokolo Yeah true, ?Type is kind of redundant now, although a nice shorthand.

    Being able to use if for things like Type | false and Type | ErrorType is kind of neat though.
  • 1
    @Lensflare @Oktokolo

    Also, I have to say I vastly prefer the Haskell-style type notation over the Kotlin/Rust/Swift one.

    Collection<User>

    Would be a collection of users, simple enough.

    In Haskell, you can define types exactly like normal functions, but with type parameters and type return... kinds (the "types of types" are called kinds)

    So you can do stuff like define a general tree, where each point is either a Leaf<T>, or an Array<Tree<T>>):

    data Tree t = Leaf t | Node [Tree t]

    And then reuse that:

    type listOfTreesOfIntegers = [Tree Integer]

    type TreeOfListsOfMaybeIntegers = Tree [Maybe Integer]

    But more than that -- you can actually use the puzzle parts of ADTs to construct values in very neat ways:

    Node (map Leaf [1,2,3])

    maps type Leaf over a list of values, so it creates a list of leaves, and puts those inside a node, which through type inference is considered to have type "Tree Num"
  • 0
    @Lensflare @Oktokolo

    And this principle of composable types is used in Haskell to the extent that there are types for things like HTML.

    type-of-html for example does not just put an HTML string in a container and label it "HTML".

    It is a complete, recursive type, codifying the complete whatwg HTML spec. If your HTML contains a typo, it fails when compiling.
  • 1
    @bittersweet
    Algebraic data types are nice and all. But they surely won't happen in PHP. Same for type inference (some IDEs already do a pretty good job though).

    And generics also would be a real stretch actually. But they might be feasible when the new JIT compiler becomes a full compiler...
  • 1
    @bittersweet that looks very neat indeed.
    It also looks like you could compose a type by combining Sum and Product types with | and &.
    Is that correct?

    Swift is not that far off, though.

    enum Tree<T> {
    case leaf(T)
    case node(Tree)
    }

    let listOfTrees: [Tree<Int>]

    But you can't do it inline and anonymously (like you can with tuples). Haskell is very interesting.
  • 1
    @bittersweet for the HTML part:
    Swift has a library named Plot (on github) which does exactly what you have described :)
    A type safe and self documenting HTML composing language all in one single HTML type! 😄
    I love this kind of stuff!
Add Comment