Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Result type function #72

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions docs/result-type-operator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# `result` type function

## Summary

This RFC proposes the addition of a new type function `result`, which can be used to make result types in luau. That are consistent with the idiom established by the [pcall and xpcall global functions](https://luau.org/library#global-functions) in the language currently.

## Motivation

Currently, in order to properly type out a result one would have to write the following:

```luau
type Mrow = ((meow: string, mrrp: number) -> (true, string)) &
((meow: string, mrrp: number) -> (false, nil))

--[[
Note: the function is being cast to any,
as this rfc is being written with the assumption the developer is using --!strict mode
--]]
local mrow: Mrow = (function(meow: string, mrrp: string)
if math.random() > .5 then
return true, `{meow} {mrrp}`
else
return false, nil
end
end) :: any
```

Due to to that making the developer have to write an overloaded function, and with overloaded functions not consistently working. Most will instead do this:

```luau
type Mrow = (meow: string, mrrp: number) -> (boolean, string?)
```

Leading to having the type burden passed onto the developer using the function, as now they have to cast the result if they want to avoid type errors:

```luau
local success, result = mrow("cat food", ":3")

if success then
local new_result: string = result :: any
print(`new food of type: {new_result}!`)
else
error("no food :(")
end
```

Some may take a diffrent approach by making their own result type, where they break from the luau idiom:

```luau
type Result<S, F = nil> = {
ok: true,
value: S
} | {
ok: false,
value: F
}

local function mrow(meow: string, mrrp: string): Result<string>
if math.random() > .5 then
return { ok = true, value = `{meow} {mrrp}` }
else
return { ok = false, value = nil }
end
end
```

## Design

The functionality of the `result` type function is special, with it being an exception as it'll create a type-pack union. Thus using the result type function will not require the developer to write an overloaded function type.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type pack unions do not exist in Luau. So, there's no way for a type pack function to produce a type pack union.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but result would be special cased. As its the only use for typepack unions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's not a way for us to do that right now. Type functions and type pack functions have to return types or type packs, and type packs are not unions. Doesn't mean we can't do this at all, but it does mean that this is essentially the same amount of work as proposing general support for type pack unions.


```luau
type result<S..., F...>
```

```luau
local function mrow(meow: string, mrrp: string): result<string>
if math.random() > .5 then
return true, `{meow} {mrrp}` -- wont error

return true, nil -- Error message: type 'nil' is not of type 'string'
else
return false, nil
end
end
```

## Drawbacks

This type function will need to be special cased, complicating maintenance for the type solver. But, having a result type would allow for pcall and xpcall to get proper types in the future. Where calls to `error` within a function being pcalled would set the second typepack in the result type to the type of the value `error` was called with.

```luau
local function mrow(meow: string, mrrp: string): string
if math.random() > .5 then
return `{meow} {mrrp}`
else
error("no cats allowed")
end
end

-- inferred as result<string, string>
local success, result = pcall(mrow, "cat food", ":3")
```

## Alternatives

Allow for type-pack unions to be written by developers, with the syntax probably looking like this:
```luau
local function mrow(meow: string, mrrp: string): (true, string) | (false, nil)
-- code here
end
```
With this example breaking backwards compadibility with some types developers may have written already, as today the following is allowed:
```luau
-- inferred as: ((meow: string, mrrp: string) -> (true, string)) | false
type mrrp = (meow: string, mrrp: string) -> (true, string) | false
```
Although it should be mentioned that result types are the only valid usecase for type-pack unions, and just having a result type instead of general type-pack union syntax would remove a potential footgun. Of someone using type pack unions for something thats not a result type.

Do nothing, and leave it up to developers if they want to write overloaded functions, or make their own result type.