From b80a7bd3d8c371d7211c794a35fb0e57cb6b1cd7 Mon Sep 17 00:00:00 2001 From: Max VelDink Date: Sat, 27 Jan 2024 06:37:20 -0500 Subject: [PATCH] refactor!: Make `Typed::Result` sealed 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. --- lib/sorbet-result.rb | 1 + lib/typed/failure.rb | 81 ------------------ lib/typed/result.rb | 155 ++++++++++++++++++++++++++++++++++ lib/typed/success.rb | 80 ------------------ test/test_data/failure.out | 6 +- test/test_data/flow_typing.rb | 14 +++ test/test_data/success.out | 4 +- 7 files changed, 175 insertions(+), 166 deletions(-) delete mode 100644 lib/typed/failure.rb delete mode 100644 lib/typed/success.rb diff --git a/lib/sorbet-result.rb b/lib/sorbet-result.rb index 4e56d12..6306fcd 100644 --- a/lib/sorbet-result.rb +++ b/lib/sorbet-result.rb @@ -5,6 +5,7 @@ require "sorbet-runtime" require "zeitwerk" +require_relative "typed/result" # Sorbet-aware namespace to super-charge your projects module Typed; end diff --git a/lib/typed/failure.rb b/lib/typed/failure.rb deleted file mode 100644 index 4ee77d9..0000000 --- a/lib/typed/failure.rb +++ /dev/null @@ -1,81 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -module Typed - # Represents a failed result. Contains error information but no payload. - 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 diff --git a/lib/typed/result.rb b/lib/typed/result.rb index dbee103..73f6f9a 100644 --- a/lib/typed/result.rb +++ b/lib/typed/result.rb @@ -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 @@ -9,6 +12,7 @@ class Result extend T::Generic abstract! + sealed! Payload = type_member(:out) Error = type_member(:out) @@ -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 diff --git a/lib/typed/success.rb b/lib/typed/success.rb deleted file mode 100644 index 6677dcc..0000000 --- a/lib/typed/success.rb +++ /dev/null @@ -1,80 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -module Typed - # Represents a successful result. Contains a payload and no error information. - 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 -end diff --git a/test/test_data/failure.out b/test/test_data/failure.out index 5acb114..4fae552 100644 --- a/test/test_data/failure.out +++ b/test/test_data/failure.out @@ -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") diff --git a/test/test_data/flow_typing.rb b/test/test_data/flow_typing.rb index fe7f691..1cea0ea 100644 --- a/test/test_data/flow_typing.rb +++ b/test/test_data/flow_typing.rb @@ -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 diff --git a/test/test_data/success.out b/test/test_data/success.out index cc26980..208a434 100644 --- a/test/test_data/success.out +++ b/test/test_data/success.out @@ -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: