Boolean Blindness


Common Definition

When a function operates on a boolean we loose information of what that value represents. Consider in Haskell:

Haskell
filter :: (a -> Bool) -> [a] -> [a]

The above can be confusing for people not familiar. Check Haskell Function Signature.

Haskell
filter even [1, 2, 3, 4, 5, 6]
-- will this print
-- 2, 4, 6
-- or
-- 1, 3, 5
-- ?

It's hard to say. The Bool is defined as

Haskell
data Bool = False | True

A more meaningful name would be

Haskell
data Keep = Drop | Take
filter :: (a -> Keep) -> [a] -> [a]

Loosing Information

Other aspect that bothers me is how we lose information when using booleans. To give an example image we have this function:

Typescript
function canUserAccessVideo(userId: number, videoId: number): boolean {
  if (
    isVideoBlocked(videoId) ||
    isUserBlockedByVideoAuthor(userId, videoId) ||
    isVideoBlockedInUserRegion(userId, videoId)
  ) {
    return false;
  }
  return true;
}

By returning boolean the function is only able to answer if the user can access the video or not. As often happens, things will change and we might be interested in knowing the reason why the user can't access the video. To provide that answer we have to change the function signature. In a bigger project this may touch several files and it's not a trivial change.

I prefer to never be in that corner in the first place and instead avoid boolean from the beginning. An alternative is to use a robust type: (types are not necessary, but in this context I enjoy having them)

Typescript
enum AccessDeniedReason {
  VIDEO_IS_BLOCKED,
  USER_BLOCKED_BY_VIDEO_AUTHOR,
  VIDEO_BLOCKED_IN_USER_REGION,
}

type BooleanResult<T> = {
  isSuccess: boolean;
  reason?: T;
};

function canUserAccessVideo(
  userId: number,
  videoId: number
): BooleanResult<AccessDeniedReason> {
  if (isVideoBlocked(videoId)) {
    return { isSuccess: false, reason: AccessDeniedReason.VIDEO_IS_BLOCKED };
  }
  if (isUseBlockedByVideoAuthor(userId, videoId)) {
    return {
      isSuccess: false,
      reason: AccessDeniedReason.USER_BLOCKED_BY_VIDEO_AUTHOR,
    };
  }
  if (isVideoBlockedInUserRegion(userId, videoId)) {
    return {
      isSuccess: false,
      reason: AccessDeniedReason.VIDEO_BLOCKED_IN_USER_REGION,
    };
  }
  return { isSuccess: true };
}

It is a lot more code. However, besides giving the information if it can access the video it also gives the reason for the negative cases. The original requirement is only to know if the user can or can't access the video, so we can create a generic function to convert the type above to a boolean:

Typescript
function toBoolean<T>(arg: BooleanResult<T>) {
    return arg.isSuccess ? true : false;
}

toBoolean(canUserAccessVideo(1, 2))