Options - A better way for nil-checking

Options - A better way for nil-checking

Introduction

Options are a way to detect two different types of values - Some and None. Some is a type that shows that it contains a value, while None contains nil (or no value at all). These force you to account for nil errors in a good way - nil could break your code if you are not careful with it.

Nil/null can be considered as a “billion-dollar mistake”, as said by Tony Hoare, the creator of nil when working on a programming language:

“In 1965, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

Options solve this problem by acting as a barrier between an actual value & nil. That way, it prevents you from making mistakes and encountering these errors. You need to unwrap the Option it to get the actual value aside from nil, if it unwrapped into nil, then it’ll error out.

A good example of Options being used is in the Rust programming language. Rust doesn’t have a nil type - it instead uses the Option type for detecting if there’s a value or no value.

This is a simple example to show how Options work.


fn main() {
    let none: Option<i32> = None; // this option doesnt contain anything
    let some: Option<i32> = Some(10); // this option contains an 32-bit integer

    some.unwrap(); // this will work since there is an value
    none.unwrap(); // this will panic (or error out) in rust since it unwrapped a None value
}

Usage

Setup

So, how do we make use of Options in Lua? There is a module that is similar to this by sleitnick as part of the Knit framework.

Copy and paste the code from here into a ModuleScript in Studio (or, if you are using Rojo, copy and paste this into a newly created .lua file from the Option module source code. It’s recommended to name this module “Option” and place this into ReplicatedStorage.

Now, for the fun part: programming this ourselves!

Programming

One of the most used methods where it could return nil is two of HttpService’s functions - GetAsync & JSONDecode. For example, say that we are sending a GET request to https://yesno.wtf/api to answer a question that the user typed in and uses the result from the API to answer the user’s question.

But what if we somehow messed up the request, or the server messed it up? Well, this is where Options come into play. We should use Options when the result from JSONDecode could be nil because it could either:

  1. Failed to parse the JSON
  2. The request got messed up from the HTTP client/server

So, this means we should return a None value if JSONDecode failed to parse the JSON, else, we’ll return a Some value with the response from the HTTP server.

Now, create a server script and enter the following code:

local HttpService = game:GetService("HttpService")

local Option = require(path.to.option) -- This should be replaced where the Option module is at!

local URL = "https://yesno.wtf/api"
local NOT_A_QUESTION_ERROR = 'Not a question'

local function AnswerQuestion(question)
	if string.find(question, '?') == nil then
		return Option.Some(NOT_A_QUESTION_ERROR) -- We want to return an error that isn't a None value to inform the user of an error!
	end
	
	local response, result
	
	pcall(function()
		response = HttpService:GetAsync(URL)
		result = HttpService:JSONDecode(response)
	end)
	
	if not result then
		return Option.None -- Our JSON failed to be encoded or the request got messed up
	end
	
	return Option.Some(string.format("The answer to that question is %s", result.answer)) -- Everything's OK, let's get the result
end

local answer = AnswerQuestion("Is this tutorial cool?")

if answer:IsNone() then
	print('An error occured while processing the question') -- Inform the user about the error
else 
	local result = answer:Unwrap()
	
	if result == NOT_A_QUESTION_ERROR then -- The input isn't a question, inform the user about that
		print(NOT_A_QUESTION_ERROR)
		return
	end
	
	print(result) -- Everything's OK, get the answer to that question
end

And there you have it! You’ve now learned about Options and learned how to use them! Hopefully, this tutorial was helpful, give feedback down in the replies if I missed something/have a correction about this.

9 Likes

image

11 Likes

You could use that, but here’s an issue: you don’t have control over what assert returns. With options, you have control of what a function could return nil or false.

1 Like

The point is to throw an error to make you know that your code has an issue. If you want to return a value then just use an if statement.

if (not Variable) then
     return "oh no variable is nil D::::"
end

I don’t see a point into trying to make Lua a new language. Lua isn’t Rust. Lua is Lua. Just use a different language with the features you want, and transpile it to Lua.

image
image

1 Like

I have to be honest, I’m a little confused as to what the point of the Options module is, and why you’d use it over simple if statements or assert. I don’t see what real use cases this has. The example provided isn’t something I’d expect most developers to be doing.

It would help to show an everyday use case of this module; something every developer does that could be avoided or handled better by using this module.

5 Likes