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:
- Failed to parse the JSON
- 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.