Created
July 3, 2021 12:49
-
-
Save dorianmariecom/b7ce6f9cb5328b38bcdf52d2f491ab63 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "rspec" | |
class Class | |
def |(other) | |
Type.new(self, other) | |
end | |
def class_of?(object) | |
object.is_a?(self) | |
end | |
end | |
class Type | |
class Error < Exception | |
end | |
def initialize(*types) | |
@types = types | |
end | |
def class_of?(object) | |
@types.any? { |type| type.class_of?(object) } | |
end | |
end | |
def check_types(method_name, locals, types) | |
locals = Array(locals) | |
types = Array(types) | |
[locals.size, types.size].max.times do |index| | |
if !types[index].class_of?(locals[index]) | |
raise Type::Error | |
end | |
end | |
end | |
def add(a1, a2) | |
check_types(:add, [a1, a2], [Integer, Integer]) | |
a1 + a2 | |
end | |
class User | |
def name | |
end | |
def fetch | |
end | |
end | |
def fetch(user) | |
check_types(:fetch, user, User) | |
user.fetch | |
end | |
def username(user) | |
check_types(:username, user, User | NilClass) | |
user&.name | |
end | |
RSpec.describe do | |
it { add(10, 5) } | |
it { expect { add("cool", 243) }.to raise_error(Type::Error) } | |
it { expect { add(nil, 243) }.to raise_error(Type::Error) } | |
it { fetch(User.new) } | |
it { expect { fetch(nil) }.to raise_error(Type::Error) } | |
it { username(User.new) } | |
it { username(nil) } | |
it { expect { username(123) }.to raise_error(Type::Error) } | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I played with it a bit and came up with this:
Description of refactorings
class_of?
for===
b/c then it will work for anything you can put in a case statement, eg procs and modules.Type
class toType::Or
b/c you could also imagine wanting multiple constraints, leading to aType::And
class_of?
and now with===
it will incorrectly pass if the local isnil
, b/cruby -e 'p 2.times.all? { |i| [Integer][i] === [1, nil][i] }'
but that should have been a wrong number of arguments.Array(locals)
because an arg ofnil
or[]
should be[nil]
and[[]]
, butArray(locals)
would change them both to[]
, changing the number of locals. Decide whether to do this or not based on the types, since the types are given explicitly and the locals are forwarded from the caller.zip
since we now know they have the same sizeType::Error
inherits fromStandardError
instead ofException
Type::Error
can be defined in a single lineError = Class.new StandardError
|
method toType
, to avoid monkey patchingType::Error
in favour ofTypeError
because there's already an exception for it, and people will expect it to raise that error, and b/c I want to be able to includeType
, butError
feels too generic of a name to include into other contexts. Then includeType
intoClass
to keep everything working.Type
fromClass
intoModule
so you can do things likeComparable | Enumerable
Type
inclusion intoModule
happen in a refinement so that there is no modification of core classes, add ausing Type
to allow everything to continue working.check_types
into its own Module and include it onto Kernel in a refinement, again, to avoid monkey patchingcaller_name
method (note that it's super expensive), and set it as the default value ofcheck_types
first argument,method_name
, so that you can omit the method name and it will be calculated.Types::Check
onto itself so we can callcaller_name
fromcheck_types
(since this happens in a refinement, it's lexically scoped, socheck_types
can't see thatTypes::Check
is included intoKernel
, so it can't callcaller_name
... also maybe a bug) However, sincecaller_name
is private,check_types
can't sayTypes::Check.caller_name
... so usepublic_class_method
to make it public onTypes::Check
, but private on things thatTypes::Check
is included into.Final result