Skip to content

Commit

Permalink
refactor!: Make Typed::Result sealed
Browse files Browse the repository at this point in the history
We only ever want there to be two inheritors of `Typed::Result`, `Success` and `Failure`. We can express this in Sorbet with the `sealed!` helper. In order to use this, we must move the definitions of `Success` and `Failure` into the same file as `Result`. See https://sorbet.org/docs/sealed for more info.
  • Loading branch information
maxveldink committed Jan 27, 2024
1 parent 71bc7ef commit b80a7bd
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 166 deletions.
1 change: 1 addition & 0 deletions lib/sorbet-result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

require "sorbet-runtime"
require "zeitwerk"
require_relative "typed/result"

# Sorbet-aware namespace to super-charge your projects
module Typed; end
Expand Down
81 changes: 0 additions & 81 deletions lib/typed/failure.rb

This file was deleted.

155 changes: 155 additions & 0 deletions lib/typed/result.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# typed: strict
# frozen_string_literal: true

require_relative "no_error_on_success_error"
require_relative "no_payload_on_failure_error"

module Typed
# A monad representing either a success or a failure. Contains payload and error information as well.
class Result
Expand All @@ -9,6 +12,7 @@ class Result
extend T::Generic

abstract!
sealed!

Payload = type_member(:out)
Error = type_member(:out)
Expand Down Expand Up @@ -55,4 +59,155 @@ def on_error(&block)
def payload_or(value)
end
end

class Success < Result
extend T::Sig
extend T::Generic

Payload = type_member
Error = type_member { {fixed: T.noreturn} }

sig { override.returns(Payload) }
attr_reader :payload

sig do
type_parameters(:T)
.params(payload: T.type_parameter(:T))
.returns(Typed::Success[T.type_parameter(:T)])
end
def self.new(payload)
super(payload)
end

sig { returns(Typed::Success[NilClass]) }
def self.blank
new(nil)
end

sig { params(payload: Payload).void }
def initialize(payload)
@payload = payload
super()
end

sig { override.returns(T::Boolean) }
def success?
true
end

sig { override.returns(T::Boolean) }
def failure?
false
end

sig { override.returns(T.noreturn) }
def error
raise NoErrorOnSuccessError
end

sig do
override
.type_parameters(:U, :T)
.params(block: T.proc.params(arg0: Payload).returns(Result[T.type_parameter(:U), T.type_parameter(:T)]))
.returns(Result[T.type_parameter(:U), T.type_parameter(:T)])
end
def and_then(&block)
block.call(payload)
end

sig do
override
.params(_block: T.proc.params(arg0: Error).void)
.returns(T.self_type)
end
def on_error(&_block)
self
end

sig do
override
.type_parameters(:Fallback)
.params(_value: T.type_parameter(:Fallback))
.returns(T.any(Payload, T.type_parameter(:Fallback)))
end
def payload_or(_value)
payload
end
end

class Failure < Result
extend T::Sig
extend T::Generic

Payload = type_member { {fixed: T.noreturn} }
Error = type_member

sig { override.returns(Error) }
attr_reader :error

sig do
type_parameters(:T)
.params(error: T.type_parameter(:T))
.returns(Typed::Failure[T.type_parameter(:T)])
end
def self.new(error)
super(error)
end

sig { returns(Typed::Failure[NilClass]) }
def self.blank
new(nil)
end

sig { params(error: Error).void }
def initialize(error)
@error = error
super()
end

sig { override.returns(T::Boolean) }
def success?
false
end

sig { override.returns(T::Boolean) }
def failure?
true
end

sig { override.returns(T.noreturn) }
def payload
raise NoPayloadOnFailureError
end

sig do
override
.type_parameters(:U, :T)
.params(_block: T.proc.params(arg0: Payload).returns(Result[T.type_parameter(:U), T.type_parameter(:T)]))
.returns(Result[T.type_parameter(:U), Error])
end
def and_then(&_block)
self
end

sig do
override
.params(block: T.proc.params(arg0: Error).void)
.returns(T.self_type)
end
def on_error(&block)
block.call(error)
self
end

sig do
override
.type_parameters(:Fallback)
.params(value: T.type_parameter(:Fallback))
.returns(T.any(Payload, T.type_parameter(:Fallback)))
end
def payload_or(value)
value
end
end
end
80 changes: 0 additions & 80 deletions lib/typed/success.rb

This file was deleted.

6 changes: 3 additions & 3 deletions test/test_data/failure.out
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ test/test_data/failure.rb:26: Expected `Integer` but found `String("error")` for
26 | Typed::Failure[Integer].new("error")
^^^^^^^
Expected `Integer` for argument `error` of method `Typed::Failure#initialize`:
./lib/typed/failure.rb:30:
30 | sig { params(error: Error).void }
^^^^^
./lib/typed/result.rb:162:
162 | sig { params(error: Error).void }
^^^^^
Got `String("error")` originating from:
test/test_data/failure.rb:26:
26 | Typed::Failure[Integer].new("error")
Expand Down
14 changes: 14 additions & 0 deletions test/test_data/flow_typing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,18 @@ def test_flow
T.assert_type!(result.error, String)
end
end

sig { void }
def test_case
result = do_something(true)

case do_something(true)
when Typed::Success
T.assert_type!(result.payload, Integer)
when Typed::Failure
T.assert_type!(result.error, String)
else
T.absurd(result)
end
end
end
4 changes: 2 additions & 2 deletions test/test_data/success.out
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ test/test_data/success.rb:26: Expected `Integer` but found `String("success")` f
26 | Typed::Success[Integer].new("success")
^^^^^^^^^
Expected `Integer` for argument `payload` of method `Typed::Success#initialize`:
./lib/typed/success.rb:30:
30 | sig { params(payload: Payload).void }
./lib/typed/result.rb:87:
87 | sig { params(payload: Payload).void }
^^^^^^^
Got `String("success")` originating from:
test/test_data/success.rb:26:
Expand Down

0 comments on commit b80a7bd

Please sign in to comment.