Much ado about null

in PHP7 months ago (edited)

null has a controversial history. It's been called "the billion-dollar mistake" by its creator. Most languages implement null, but those few that do not (such as Rust) are generally lauded for their smart design by eliminating a class of errors entirely. Many developers (myself included) have argued that code that uses null or nullable parameters/returns is intrinsically a code smell to be avoided, while others (also myself included, naturally) have argued that is too draconian of a stance to take.

Anna Filina has a very good three-part series (properties, parameters, and returns) on how to avoid null in PHP generally. Alexis King has a related post (excuse the Haskell) on how to avoid needing edge cases like null in the first place.

However, I want to go a bit deeper and try to understand null from a different angle, and tease out the nuance of why one would really want to use it, and thus what we should be doing instead. To get there we'll have to go through a tiny little bit of type theory, but it will be quick and painless, I swear.

A language-specific problem

One of the difficulties of null is that most languages have a feature called null (or sometimes nil, just to be confusing), but that doesn't necessarily mean the same thing in all languages. That would be too easy. For instance, what Tony Hoare called his "billion-dollar mistake" was not null itself! It was null-references. Specifically, in the design of Algol 60 it was convenient to allow a pointer to point to address 0 to indicate "actually this pointer is invalid." That easy shortcut made implementation convenient, but also meant that every single time you wanted to use a pointer you had to first ensure it pointed at something other than address 0, or Weird And Dangerous Things(tm) could happen.

At the time, many developers favored "just let my code run" rather than "prevent me from shooting myself in the foot", so that idea was preferable. After a billion perforated feet language designers generally know that's a bad idea, but many developers, even seasoned developers, still would rather blow their own foot off so null pointer references and similar foot-guns still exist. Java, for instance, makes all objects nullable, which is effectively the same thing.

I am not here to defend null pointer references. They are already buried.

A wee bit of types

Instead, I want to talk about null in languages where nullability is optional and opt-in, such as PHP. And for that, we need to first talk about what it means for null to be a type, and what it means for anything to be a type.

To do that, we need to start with some terminology that will drive the rest of this discussion.

  • A Type is a set of possible values. It's a logical list. Those values could be finite or infinite. For instance, int is the type (set) of all integers, which is nominally infinite. bool is the type (set) of just the values true and false, which is finite.
  • A Product Type is a compound type that is composed of two or more other types, next to each other. For instance, "one int and one bool" is a product type. In most languages today this is represented by a class.
  • A Union Type is a type whose list of possible values is simply the combination of two other types. int|bool contains all possible values of int, plus all possible values of bool. However, there may not be any intrinsic tracking of which subset the value came from (whether it's an int or a bool).
  • A unit type is a finite type that has only one legal value. While technically one can have more than one unit type (e.g., multiple enums with only a single case), a unit type by definition carries no useful information so all unit types are logically equivalent.
  • A bottom type is a finite type that has zero legal values. It is, technically, a subtype of every other possible type (since all of its possible values are also valid values of all other types, on the technicality that it has no possible values).
  • A top type is a type that accepts literally anything. It is, technically, a super-type of every other possible type.

It may not be common to think of PHP in these terms, but PHP has every single one of these.

  • It has a list of predefined types, and user code can define additional product types called classes.
  • Although PHP cannot, yet, define a free-standing union type, we can define a union type for a property, parameter, or return value.
  • PHP supports enums, which are a way to define a custom type with a finite list of possible values.
  • never is the PHP bottom type. There is no value that is of type never, but you can use it as a return type to indicate that a function never has a return value.
  • The mixed type is PHP's top-type. Every value passes a type-check for mixed. Omitting a type in any location is equivalent (from a type theory perspective at least) to declaring it mixed.
  • And PHP has a unit type, called null. Its only legal value is... null. Yes, null refers to both the type and the value. It's only slightly confusing (and common).

PHP has no concept of a null pointer reference, so the "billion-dollar mistake" security hole that is null pointer references does not apply. Why, then, is null still problematic in PHP, especially when recent versions have given us such great new tools for working with null like ?? or ?->?

To answer that, we need to talk a little bit about functions. (For this discussion, "function" always includes "method"; everything we're going to say about one applies to the other.)

  • A function is an operation from an Input to an Output. Both the Input and the Output have some known Type. Even if you don't specify a type, PHP interprets that the same as if you'd specified mixed, the top type.
  • The fancy name for the input of a function is its domain. The fancy name for its output is range or codomain.
  • A total function is a function that is defined for every possible input. That is, any input you feed it that matches the specified input type has a corresponding output value. add(int, int): int is a total function over integers, because for any possible integers you feed into it there is an integer that it can return.
  • A partial function is a function that is not defined for every possible input. That is, there are values that pass the type check of the inputs for which no reasonable output is possible. divide(float, float): float is a partial function, because there are float values you can pass to it, specifically 0, for which there is no meaningful return value.

With all that laid out, here's the key observation that is going to drive the rest of our discussion:

Total functions require less error handling than partial functions

Total functions and errors

By definition, a total function has a meaningful return value for every possible input. That means there is, by definition, no possible input you can provide for which a meaningful return value isn't possible.

We don't need to check the return value of add(int, int): int to ensure that it's an int, or that there was such an integer, or that there was a database connection failure, or anything else along those lines. If the function truly is total, then it can only fail in one of two ways: Either the parser/compiler complains beforehand (because I tried to pass a string to it, which is invalid input), or I will get a runtime error (because the whole system is broken and therefore all bets are off). "If it compiles then it's right" is the goal, and while we can never truly achieve that, total functions get us a long way toward that goal.

A key value proposition for robust type systems in code is to make it possible to represent more operations as total functions. For example, if we had the ability to define a type for "a floating point number other than 0", then divide(float, nonZeroFloat): float would be a total function. We've now moved the need to error check that potential 0 value out of the code and into the definition of the problem itself. (That kind of highly complex type rule is known as "Dependent type theory" and is off-topic for today, so don't worry about it.)

Languages vary widely in how precise and nuanced you can get in your type definitions. PHP 8.1 is, I would argue, somewhat middle of the road overall, though one of the more advanced among interpreted languages.

Partial functions and errors

By definition, a partial function has values that are legal inputs, for which there is no meaningful output. That means if a legal but unmatched input is provided, the function needs to... do something else. That "something else" is error handling, and it takes many forms. I won't go into all the possible forms of error handling here (that's an article unto itself, at least), but generally speaking there's two popular approaches: Return a sentinel value or throw an exception.

A sentinel value is a value that is a legal output in itself, but the developer knows out-of-band has special meaning. For example, "returns 0 on error" from a function that returns integers is using 0 as a sentinel value, because it's a legal integer but has an extra special "guarded" meaning. (Hence the name "sentinel," as in a guardian.) The caller needs to always remember, however, that 0 has that special meaning. Or, perhaps, that 0 may have that special meaning. Depending on the function, 0 may also be a legal non-sentinel value. And that's where trouble happens.

One could argue that a partial function that returns a sentinel value is, strictly speaking, now a total function, since it has a return value. However, while the return value is within the type universe allowed, it is semantically still only partial because not every input has a semantically meaningful return, even if it has a compiler-safe return.

The other common option is to throw an exception. An exception is a typed value that may carry additional context information, but more importantly breaks the flow of control. An exception is a statement of "something is totally broken and I don't know WTF to do with it, halp!". It's appropriate when a partial function hits a case that the developer didn't anticipate or has no way of handling in a reasonable way, so gives up.

Exceptions are very expensive in some languages, including PHP. They're also destructive. You don't necessarily know that the state of the program is now reasonable. It may or may not be safe to continue doing anything. While you, the code writer, may know from context that a particular exception is non-world-destroying, there is no systematic way for the program to know that, the way it can know that a total function is always safe.

null as a sentinel

One way of avoiding the problem of sentinel values sharing the same type as real, meaningful return values is to expand the function's codomain (return types). For instance, if we could instead have a codomain that spanned multiple types, as a union, we could say that return values in one set are "real" return values and those in the other set are error return values.

A nullable type in PHP, like ?int, is in practice a shorthand for the union type int|null. That is, we're making a union of int and the unit type, which gives us as potentially legal values "all integers that exist... plus null". At least from a type theory perspective, that converts a partial function into a total function. divide(float, float): float|null is a total function, in that for any two input floats there is always a possible output that is either float or null.

That allows us to use null as a sentinel value, reserving all possible floating point values for actual, meaningful results.

Very old parts of the PHP standard library, which dated from before PHP had explicit types at all, often used false as a sentinel value. For example, strpos() technically has a return type of int|false, because its output range is any integer, or a boolean, specifically false. (The only reason PHP has false as a declarable type is to support these old and horrible functions. Do not use it yourself. Ever. If you do, you are wrong. There is no exception to that statement.) Of course, PHP being weakly typed 0 (which is an entirely reasonable return value from strpos()) and false are equivalent (==), leading to all sorts of stupid bugs.

This is why sentinel values that are also equal or equivalent to a meaningful output value are a very bad idea.

The problem with null

Using the unit type as a sentinel has its advantages, simplicity being key among them.

Except... a null may not be a valid input for the next function you want to use that value in. So this kind of return type widening technically doesn't render error handling unnecessary, it externalizes it from the function. Which... can still be a win, but not quite as big a win as we made it sound originally. Aw.

What we have, then, is null, via a union type, as the universal sentinel value. It's a built-in readily available type/value that indicates "there was no otherwise meaningful output for the type-safe input you provided." But... it doesn't tell you more than that.

The problem with null is that it's a universal sentinel value. In context, it could mean

  • your input was invalid in some way the type system cannot capture.
  • your input was invalid due to some external constraint (eg, not found in a database).
  • there is no value here that exists, and that's OK according to our business rules.
  • no action needs to be taken
  • this value hasn't been provided yet, but it will be later, and we've accounted for that (or possibly not)
  • various other meanings

Just null on its own doesn't differentiate between these possible meanings. Moreover, it could mean different things for the same value when passed or returned to two different functions. To divide(float, float): float|null it means "input invalid", but to setRate(float|null): void it means "use default". But is that really correct? Should invalid input silently translate to using a default value? Maybe, but probably not.

The root issue here is that null is insufficiently expressive of all the ways in which a function could be partial, so using it as a universal sentinel to convert a partial function to a total function is still a foot-gun.

Because it is the de facto universal sentinel, though, PHP (like many languages) has added several native language features to make working with it easier. Of note, ?? and ??= make providing a fallback value for null easier, while ?-> makes propagating a null easier.

However, the latter is extremely problematic precisely because that propagated null may change semantic meaning from one function to another.

Checking sentinels

Sentinel values, or error returns more generally, have an annoying problem that if you don't always check for them then they're useless or worse; but always checking for them, when they are rarely returned, is costly both for developer time and execution time. But you can guarantee the one time you forget to check it is the one time it's going to be critically necessary.

Consider the square-root function.

  • sqrt(int): float - This is a partial function, and if passed a negative value the program must die painfully (or ideally not even compile).
  • sqrt(int): float|null - This is a technically total function, but you must always check that the return value is not null, and a simple truthiness check won't work because null == 0 (in PHP; it does different weird things in other languages) and 0 is an entirely legal return value.
  • sqrt(int): float throws \InvalidArgumentException - This is a partial function, and you know that if the function returns then the return value is valid. However, if a negative number is passed the program enters a state of ¯\_(ツ)_/¯. Moreover, someone still needs to catch that exception somewhere or the whole program will die painfully.

Checking exceptions

Exceptions, as noted, are not an error condition. They are a failure condition. They indicate a case that the author could not foresee and thus could not handle, and thus nothing is trustworthy anymore.

At the risk of sounding judgemental, if you're writing a sqrt() function and didn't foresee someone passing in a negative value, the problem exists between the chair and keyboard. That is a very foreseeable circumstance, and as a responsible programmer you should write some error path for it of some kind. So no, exceptions are not appropriate here.

A concrete example

To use an example most of us will be familiar with, consider these two functions:

function getProduct(int $id): Product {}

function getProductsByColor(string $color): array {}

As written, it's fairly self-evident what they do in the happy path: The first returns a single Product object, the second returns a list of Product objects. But there are far more unhappy paths than happy paths (true of nearly any meaningful code), so let's consider those.

  1. If getProduct() is called with a non-integer, that's a type error. Either the compiler or the runtime will fail out on us, which is correct because the input is not in the function's domain. All is well.
  2. If getProductsByColor() is called with a non-string, that's a type error. Either the compiler or the runtime will fail out on us, which is correct because the input is not in the function's domain. All is well.
  3. If getProduct() is called with an integer that does not map to an existing Product... we got nothing. This is bad.
  4. If getProductsByColor() is called with a string that does not correspond to a known color... we got nothing. This is bad.
  5. If getProductsByColor() is called with a string that is a valid color but corresponds to no valid products, what should happen?
  6. If the database used to store product information catches fire during the function call... this is also bad, but there's decidedly little we can do about it.

Case 6 is out of our control, and not something we should be asked to anticipate. Throwing an exception in that case is entirely reasonable.

Cases 1 and 2 are already handled for us by the type system. They are "correct by construction," which is the fancy way of saying "if it compiles, it's right (at least as far as this particular problem is concerned)." We want this. The more we can make correct-by-construction, the better.

Case 5 has a self-evident answer. An empty list is a valid and semantically meaningful return from this function, and readily representable by the type system. In fact, it already is, so no further action is needed. (But always consider this case when making sure you have your error bases covered!)

Case 4 is not immediately obvious, but we can convert it to a correct-by-construction case by changing the parameter type from string to Color. Color is an object we define that encapsulates whatever our definition of "valid color" is for this problem space. It could be a validated RGB tuple, but in this case probably an Enum is probably more correct (as our products likely come in a fixed set of colors for which we have names). This is up to your domain design.

The interesting case is case 3. The ID matches whatever format we have for Product identifiers, but there simply is no such product in the system. Now what?

The traditional approach is to do one of two things:

  1. Change the return type to ?Product (aka Product|null) and return the unit type null if there is no such product.
  2. Define a "null object" instance of Product, which is some type-specific equivalent of 0 for integers or empty string for strings, and return that, which implies some kind of default behavior for the calling code without extra code paths.

Depending on the context, a null object may or may not make sense. I would argue that the question hinges on whether the object type has a logical "0 value" that makes sense. If it does, go ahead and use it. If not, then guess what? You just reinvented a sentinel value in your own type that may or may not have all the problems of any other sentinel value. Specifically, the calling code needs to know about it and know what to do with it.

Some pundits argue that you should throw a ProductNotFoundException or similar in this case. I would argue that is wrong, as an exception is for "exceptional" unhandleable situations. "Someone put a number in the URL that I don't have" is not an unhandleable situation. It's a very normal situation, one that as a developer you can absolutely foresee, and thus handling it is entirely within your scope of responsibility. Throwing an exception for a mundane case is not design, it is punting that design responsibility to someone else and making them clean up after you in the most CPU-expensive way possible. Don't do that.

Maybe a monad would help?

Can we do better? There's two ways, I would argue, to have more robust sentinels; one traditional, one less so.

The core problem is the widened return value from a function, which could be a real value or a sentinel. That's incompatible with the next function called, which wants just the real value. We want to encapsulate that error check, or more generally we want to convert the next function from one that takes an int to one that takes "an int or the sentinel and does something logical with the sentinel." The most common way to do that is with a pattern called a "monad."

Stop, don't run! This is not a monad tutorial, and I am not going to get into category theory here. (If you want the full rundown on monads, try my book on Functional Programming in PHP.) I'm just going to show a common technique.

Specifically, there is a particularly common monad pattern called Maybe, Option, or Optional (they're all the same thing). The implementation varies quite a bit by language, often being unrecognizable in a language you're not familiar with, and in most modern OOP-centric languages there's a bunch of ways to do it. In PHP 8.1, a simple implementation could look like this:

final class Maybe
{
   private static Maybe $none;

   private function __construct(public readonly mixed $value = null) {}

   public static function of($value): self
   {
       return new static($value);
   }

   public static function none(): self
   {
       return self::$none ??= new self();
   }
  
   public function bind(callable $fn): self
   {
       if (!is_null($this->value)) {
           return $fn($this->value);
       }
       return $this;
   }
  
   public function valid(): bool
   {
       return !is_null($this->value);
   }
}

/////////////////////////////////////////////////

function getProduct(int $id): Maybe
{
   $data = getFromDatabase(/*...*/);
   if ($data) {
       return Maybe::of(new Product($data));
   }
   return Maybe::none();
}

$maybeProduct = getProduct($someId);
$maybeManager = $maybeProduct->bind(getProductManager(...));

if ($maybeManager->valid()) {
   $manager = $maybeManager->value;
} else {
   // Do some error handling.
}

What we've effectively done here is factor out the caller's necessary is_null() check into an object that does it for us. (An alternate version that relies more heavily on the type system itself is also available in the book.)

However, that changes the way we have to work with and call functions. Specifically, we use the bind() method to apply functions that expect a normal value to the maybe-value. That handles unpacking the value and skipping over null cases. When we're done, we absolutely are forced to be cognizant of the fact that the value could be missing. It's actually very similar to the new ?-> operator, but is centered on the value and functions rather than just methods.

Honestly, in PHP today a Maybe is rather clunky. Languages like Haskell have this pattern baked into the language at such a low level that you don't realize it's there. Swift and Rust have built-in operators to make working with them easier. And, most importantly, in PHP a Maybe can only be mixed. If we wanted a Maybe that kept track of the type it's wrapping and enforced that, well, now we're talking about the mythical "generics" that is unlikely to ever come to PHP despite basically everyone wanting it. (It's just really hard to do.)

The main advantage of a Maybe over a nullable is that a Maybe forces you to handle the missing-case. They are conceptually very similar, but it's hard to argue that forcing you to handle the missing-case is a good trade-off to losing type safety and needing a less self-evident syntax (since none of the built-in null related syntax works anymore).

In some sense, you can think of a nullable argument as a "naked Maybe Monad." Keep that image in the back of your head for a moment.

A more robust alternative to Maybe is a similar pattern called Either. The idea is the same, but instead of a success case with a value and a failure case that is basically a null, the failure case carries additional data. Most commonly that would be with an error message of some kind with appropriate context.

As before, there's many ways to implement it but here's one that works for now:

abstract class Result
{
   public static function of(mixed $value): static
   {
       return new static($value);
   }

   abstract public function bind(callable $fn): mixed;
}

class Success extends Result
{
   protected function __construct(public readonly mixed $value) {}
  
   public function bind(callable $fn): mixed
   {
       return $fn($this->value);
   }
}

interface Error {}

class Failure extends Result
{
   protected function __construct(public readonly Error $value) {}
  
   public function bind(callable $fn)
   {
       return $this;
   }
}

/////////////////////////////////////////////////

class NoProductFound implements Error
{
   public function __construct(public readonly int $id) {}
}

class ProductNotYetReleased implements Error
{
   public function __construct(public readonly int $id) {}
}

class NotHolidaySeason implements Error
{
}

function getProduct(int $id): Result
{
   $data = getFromDatabase(/*...*/);
   if (!$data) {
       return Failure::of(new NoProductFound($id));
   }
   if ($data['prerelease']) {
       return Failure::of(new ProductNotYetReleased($id));
   }
   return Success::of(new Product($data));
}

$sales = getProduct($someId)
   ->bind(getCurrentSales(...));

if ($sales instanceof Success) {
   $currentSales = $sales->value;
} else {
   $error = match (get_class($sales)) {
       NoProductFound::class => ...,
       ProductNotYetReleased::class => ...,
       NotHolidaySeason::class =>
   };
   // Do some error handling.
}

In this case, we use 2 separate objects to indicate if we're on the success path or failure path. The success path carries an unknown-typed object (for the same generics reason as before) and continues to apply functions to it. The failure path carries an Error-typed object and all functions become a no-op.

In this case, we get two key benefits:

  • We get multiple "out of bounds" sentinel values that are self-descriptive and do not cause confusion across domain boundaries
  • We are forced to check the value for success before using it (even if that's by using bind()).

However, there's three downsides:

  • We lose type safety on the function's return value due to the lack of generics.
  • We cannot use the convenient null related syntax.
  • We are forced to check the value for success before using it (even if that's by using bind()).

Whether being forced to check the value for success is a pro or a con depends on who you ask, I suppose. Absent built-in features like Rust's ? marker (which auto-returns the tested Result if it's an error-branch), it can get cumbersome, but it's cumbersome for a reason: These are code paths you really do need to deal with if you want robust code.

A Maybe and Either/Result monad are the typical ways of providing a better sentinel value than the unit type, at least in languages that support generics. In languages without generics they are rarely used because of the loss of type information. They are not a common pattern in PHP.

However, there is an alternative approach that offers a different set of tradeoffs that may be more palatable to PHP devs today.

A naked result

A few moments ago, we described a nullable type -- that is, one that is a union of some domain type with the unit type, null -- as a "naked Maybe monad." That's perhaps a bit of a flowery description, but a not entirely inaccurate one.

So what would a "naked Result" look like? It would look like any other union type.

Specifically, we'd be union-ing Product in this case with Error. Or, really, with whatever error type we wanted to use. In the example above the error objects included the invalid ID, but that's often unnecessary. If we don't need any context beyond the error type itself, then in PHP 8.1 we can use an enum.

enum RepositoryError
{
   case NoRecordFound;
   case AccessDenied;
   case ProductNotYetReleased;
}

function getProduct(int $id): Product|RepositoryError
{
   $data = getFromDatabase(/*...*/);
   if (!$data) {
       return RepositoryError::NoRecordFound;
   }
   if ($data['prerelease']) {
       return RepositoryError::ProductNotYetReleased;
   }
   return new Product($data);
}

$product = getProduct($someInt);

match ($product) {
   RepositoryError::NoRecordFound => /* error handling */,
   RepositoryError::ProductNotYetReleased => /* error handling */,
   default => /* It's a Product, the success case */
};

That looks really nice!

  • It's roughly the same amount of code as using the unit type, once the enum is defined.
  • We still get well-defined types.
  • Instead of checking is_null($product) we can check the value against multiple known error conditions.
  • If we're using an enum, we know the full list of potential error conditions, and they're all self-documenting.
  • If we're using an enum, match() will play nice because of its identity-based checking. (If using arbitrary objects, we'll need instanceof checks or get_class() or something.)
  • Now we get a more robust error message that contains more information than just null, which is by definition the absence of information.
  • If we're doing quick-n-dirty code and don't care about the unhappy paths we could get away with not bothering to check the return value (although that is just as wrong in production code as not checking for null). If we pass the value around somewhere unexpected it will still error out, but the error message will now be something like "function foo expected Product as its first argument, got RepositoryError" rather than "function foo expected Product as its first argument, got null".
  • We can define arbitrary sentinel values that do not impact the natural range of the function.

It would be even better if we can get tagged unions and pattern-matching into the language, but even as is it works pretty well.

What's the downside?

  • It requires more thought to figure out what the likely failure modes are. (I'm not sure if I'd call this a downside.)
  • Like null, it doesn't force us to handle error cases the way a monad does. (As before, whether this is a pro or a con is debatable.)
  • Unlike a monad, it's not really composable. We could write a lift function that wraps up the appropriate if-checks, but it would end up looking an awful lot like the Either monad we had in the first place. A generic one would be nice, but that would require a guaranteed interface/base class/trigger that could "recognize" when a value is an error case.
  • It's not compatible with the null syntax shortcuts, like ?? or ?->.

Nonetheless, I've started using this model myself and so far, I like it. It is more meaningful and robust than using the unit type (null) for all possible error cases, it prevents implicit propagation of semantically different uses of null, it's less machinery than a monad wrapper, and it still provides type safety.

Improvements

What could make this "naked either" style of error handling with union types better, given that we should assume generics aren't going to happen? There's a couple of things.

  • As noted, tagged unions would make the enum cases more robust and allow them to carry limited contextual information without having to resort to separate classes for each error case.

  • pattern-matching would combine well with that to make the error-handling match() statement more robust.

  • In PHP 8.0, throw changed from a statement to an expression, which allowed it to be used in the result arm of a match(). It might be worth considering doing the same for return, for the same reason. However, since that terminates the current stack I can also see how that might be confusing. There's a debate to be had here.

  • Perhaps most controversially, allowing objects to mark themselves (via a marker interface, probably) as being an error object could be valuable. I don't mean a Throwable, because we're not going to throw them, but mark an object (including enums) as an error object and allow tooling to be built around that in a more generic fashion. That could mean user-space tooling like this:

    function pass(mixed $val, callable $c): mixed
     {
         if ($val instanceof Error) {
           return $val;
         }
         return $c($val);
     }
    

    (Which is the core logic of both the Either and Maybe monads)

  • Or we could also look into expanding syntax tooling. For example, making ?? and ?-> recognize not just null but any Error-flagged type on the left. There's some interesting implications here to consider, not all of which I've thought through, but I do believe there's something here worth considering.

Review

This has been a long article, but what have we seen?

  • We started with an understanding of null not as a null pointer, but as the unit type, the type that has only one value.
  • We saw how that is very easy to use as a way to make partial functions into total functions, but also how that is very easily a trap. It just doesn't contain enough useful information.
  • We saw some common alternatives used by many languages, specifically the Maybe monad and an Either or Result type. Both are rather clunky in PHP, unfortunately, due to the lack of generics.
  • We saw how a "naked either" using union types and enums can provide a reasonable middle-ground between nullable values and monads, without sacrificing type information.
  • And finally, we considered some possible ways to improve the language to make "naked eithers" more ergonomic and robust.

I'd encourage folks to leverage union types and enums (or just tiny classes) to try out this "naked either" approach. Let's see if it works as well in practice for others as it has for me. If so, we can consider how to help improve them in the language itself.

Sort:  

Congratulations @crell! You have completed the following achievement on the Hive blockchain and have been rewarded with new badge(s):

You received more than 7000 upvotes.
Your next target is to reach 8000 upvotes.

You can view your badges on your board and compare yourself to others in the Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP

Support the HiveBuzz project. Vote for our proposal!

Dear @crell, we need your help!

The Hivebuzz proposal already got important support from the community. However, it lost its funding a few days ago and only needs a few more HP to get funded again.

May we ask you to support it so our team can continue its work this year?
You can do it on Peakd, ecency,

Hive.blog / https://wallet.hive.blog/proposals
or using HiveSigner.
https://peakd.com/me/proposals/199

Your support would be really helpful and you could make a difference.
Thank you!