Boolean Blindness
Common Definition
When a function operates on a boolean we loose information of what that value represents. Consider in Haskell:
filter :: (a -> Bool) -> [a] -> [a]
The above can be confusing for people not familiar. Check Haskell Function Signature.
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
data Bool = False | True
A more meaningful name would be
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:
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)
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:
function toBoolean<T>(arg: BooleanResult<T>) {
return arg.isSuccess ? true : false;
}
toBoolean(canUserAccessVideo(1, 2))