Basic type system for Ruby.
Motivation:
As a creator of Ruby gems, I have a common need that I have to handle in many of my projects: type checking of method arguments.
One of the goals of this project is to do simple type checking like "some string".is_a?(String)
, but using a bunch of basic abstractions. So, after reading this README and realizing that you need something more robust, I recommend you check out the dry-types gem.
- Required Ruby version
- Installation
- Usage
- Type checkers
- Kind::Undefined
- Kind::Maybe
- Kind::Empty
- Development
- Contributing
- License
- Code of Conduct
>= 2.2.0
Add this line to your application's Gemfile:
gem 'kind'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install kind
With this gem you can add some kind of type checking at runtime. e.g:
def sum(a, b)
Kind.of.Numeric(a) + Kind.of.Numeric(b)
end
sum(1, 1) # 2
sum('1', 1) # Kind::Error ("\"1\" expected to be a kind of Numeric")
By default, basic verifications are strict. So, when you perform Kind.of.Hash(value)
, if the given value was a Hash, the value itself will be returned, but if it isn't the right type, an error will be raised.
Kind.of.Hash(nil) # **raise Kind::Error, "nil expected to be a kind of Hash"**
Kind.of.Hash('') # raise Kind::Error, "'' expected to be a kind of Hash"
Kind.of.Hash({a: 1}) # {a: 1}
# ---
Kind.of.Boolean(nil) # raise Kind::Error, "nil expected to be a kind of Boolean"
Kind.of.Boolean(true) # true
Kind.of.Boolean(false) # false
When the verified value is nil, it is possible to define a default value with the same type to be returned.
value = nil
Kind.of.Hash(value, or: {}) # {}
# ---
Kind.of.Boolean(nil, or: true) # true
As an alternative syntax, you can use the Kind::Of
instead of the Kind.of
method. e.g: Kind::Of::Hash('')
But if you don't need a strict type verification, use the .or_nil
method.
Kind.of.Hash.or_nil('') # nil
Kind.of.Hash.or_nil({a: 1}) # {a: 1}
# ---
Kind.of.Boolean.or_nil('') # nil
Kind.of.Boolean.or_nil(true) # true
And just for convenience, you can use the method .instance?
to verify if the given object has the expected type.
Kind.of.Hash.instance?('')
# false
# ---
Kind.of.Boolean.instance?('') # false
Kind.of.Boolean.instance?(true) # true
Kind.of.Boolean.instance?(false) # true
Also, there are aliases to perform the strict type verification. e.g:
Kind.of.Hash[nil] # raise Kind::Error, "nil expected to be a kind of Hash"
Kind.of.Hash[''] # raise Kind::Error, "'' expected to be a kind of Hash"
Kind.of.Hash[a: 1] # {a: 1}
Kind.of.Hash['', or: {}] # {}
# or
Kind.of.Hash.instance(nil) # raise Kind::Error, "nil expected to be a kind of Hash"
Kind.of.Hash.instance('') # raise Kind::Error, "'' expected to be a kind of Hash"
Kind.of.Hash.instance(a: 1) # {a: 1}
Kind.of.Hash.instance('', or: {}) # {}
You can use Kind.is
to verify if some class has the expected type as its ancestor.
Kind.is.Hash(String) # false
Kind.is.Hash(Hash) # true
Kind.is.Hash(ActiveSupport::HashWithIndifferentAccess) # true
And just for convenience, you can use the method Kind.of.*.class?
to verify if the given class has the expected type as its ancestor.
Kind.of.Hash.class?(Hash) # true
Kind.of.Hash.class?(ActiveSupport::HashWithIndifferentAccess) # true
There are two ways to do this, you can create type checkers dynamically or register new ones.
class User
end
user = User.new
# ------------------------ #
# Verifiyng the value kind #
# ------------------------ #
Kind.of(User, user) # <User ...>
Kind.of(User, {}) # Kind::Error ({} expected to be a kind of User)
Kind.of(Hash, {}) # {}
Kind.of(Hash, user) # Kind::Error (<User ...> expected to be a kind of Hash)
# ---------------------------------- #
# Creating type checkers dynamically #
# ---------------------------------- #
kind_of_user = Kind.of(User)
kind_of_user.or_nil({}) # nil
kind_of_user.instance?({}) # false
kind_of_user.instance?(User) # true
kind_of_user.class?(Hash) # false
kind_of_user.class?(User) # true
# Creating type checkers dynamically is cheap
# because a singleton object is created to be available for use.
kind_of_user.object_id == Kind.of(User).object_id # true
# --------------------------------------------- #
# Kind.is() can be used to check a class/module #
# --------------------------------------------- #
class AdminUser < User
end
Kind.is(User, AdminUser) # true
Use Kind::Types.add()
. e.g:
class User
end
# You can define it at the end of the file class/module where class/module was declared.
Kind::Types.add(User)
# Or, you can add the type checker within the class definition.
class User
Kind::Types.add(self)
end
# --------------- #
# Usage examples: #
# --------------- #
Kind.of.User(User.new) # #<User:0x0000...>
Kind.of.User({}) # Kind::Error ({} expected to be a kind of User)
Kind.of.User.or_nil({}) # nil
Kind.of.User.instance?({}) # false
Kind.of.User.instance?(User) # true
Kind.of.User.class?(Hash) # false
Kind.of.User.class?(User) # true
The type checker will preserve the namespace. ;)
module Account
class User
Kind::Types.add(self)
end
end
module Account
class User
class Membership
Kind::Types.add(self)
end
end
end
Kind.of.Account::User({}) # Kind::Error ({} expected to be a kind of Account::User)
Kind.of.Account::User(Account::User.new) # #<Account::User:0x0000...>
Kind.of.Account::User.or_nil({}) # nil
Kind.of.Account::User.instance?({}) # false
Kind.of.Account::User.instance?(Account::User.new) # true
Kind.of.Account::User.class?(Hash) # false
Kind.of.Account::User.class?(Account::User) # true
# ---
Kind.of.Account::User::Membership({}) # Kind::Error ({} expected to be a kind of Account::User::Membership)
Kind.of.Account::User::Membership(Account::User::Membership.new) # #<Account::User::Membership:0x0000...>
Kind.of.Account::User::Membership.or_nil({}) # nil
Kind.of.Account::User::Membership.instance?({}) # false
Kind.of.Account::User::Membership.instance?(Account::User::Membership.new) # true
Kind.of.Account::User::Membership.class?(Hash) # false
Kind.of.Account::User::Membership.class?(Account::User::Membership) # true
The list of types (classes and modules) available to use with Kind.of.*
or Kind.is.*
are:
Kind.of.String
Kind.of.Symbol
Kind.of.Numeric
Kind.of.Integer
Kind.of.Float
Kind.of.Regexp
Kind.of.Time
Kind.of.Array
Kind.of.Range
Kind.of.Hash
Kind.of.Struct
Kind.of.Enumerator
Kind.of.Set
Kind.of.Method
Kind.of.Proc
Kind.of.IO
Kind.of.File
Kind.of.Enumerable
Kind.of.Comparable
Kind.of.Class()
Kind.of.Module()
Kind.of.Lambda()
Kind.of.Boolean()
Kind.of.Callable()
: verifies if the given valuerespond_to?(:call)
or if it's a class/module and if itspublic_instance_methods.include?(:call)
.Kind.of.Maybe()
or its aliasKind.of.Optional()
PS: Remember, you can use the Kind.is.*
method to check if some given value is a class/module with all type checkers above.
The Kind::Undefined
constant is used as the default argument of type checkers. This is necessary to know if no arguments were passed to the type check methods. But, you can use it in your codebase too, especially if you need to distinguish the usage of nil
as a method argument.
If you are interested, check out the tests to understand its methods.
If you interested in use Kind::Undefined
you can use the method .or_undefined
with any of the available type checkers. e.g:
Kind.of.String.or_undefined(nil) # Kind::Undefined
Kind.of.String.or_undefined("something") # "something"
The Kind::Maybe
is used when a series of computations (in a chain of map callings) could return nil
or Kind::Undefined
at any point.
optional =
Kind::Maybe.new(2)
.map { |value| value * 2 }
.map { |value| value * 2 }
puts optional.value # 8
puts optional.some? # true
puts optional.none? # false
puts optional.value_or(0) # 8
puts optional.value_or { 0 } # 8
#################
# Returning nil #
#################
optional =
Kind::Maybe.new(3)
.map { nil }
.map { |value| value * 3 }
puts optional.value # nil
puts optional.some? # false
puts optional.none? # true
puts optional.value_or(0) # 0
puts optional.value_or { 0 } # 0
#############################
# Returning Kind::Undefined #
#############################
optional =
Kind::Maybe.new(4)
.map { Kind::Undefined }
.map { |value| value * 4 }
puts optional.value # Kind::Undefined
puts optional.some? # false
puts optional.none? # true
puts optional.value_or(1) # 1
puts optional.value_or { 1 } # 1
You can use Kind::Maybe[]
(brackets) instead of the .new
to transform values in a Kind::Maybe
. Another alias is .then
to the .map
method.
result =
Kind::Maybe[5]
.then { |value| value * 5 }
.then { |value| value + 17 }
.value_or(0)
puts result # 42
If you don't want to use a map to access the value, you could use the #try
method to access it. So, if the value wasn't nil
or Kind::Undefined
, it will be returned.
object = 'foo'
p Kind::Maybe[object].try(:upcase) # "FOO"
p Kind::Maybe[object].try { |value| value.upcase } # "FOO"
#############
# Nil value #
#############
object = nil
p Kind::Maybe[object].try(:upcase) # nil
p Kind::Maybe[object].try { |value| value.upcase } # nil
#########################
# Kind::Undefined value #
#########################
object = Kind::Undefined
p Kind::Maybe[object].try(:upcase) # nil
p Kind::Maybe[object].try { |value| value.upcase } # nil
You can use the Kind.of.Maybe()
to know if the given value is a kind of Kind::Maybe
object. e.g:
def double(maybe_number)
Kind.of.Maybe(maybe_number)
.map { |value| value * 2 }
.value_or(0)
end
number = Kind::Maybe[4]
puts double(number) # 8
# -------------------------------------------------------#
# All the type checker methods are available to use too. #
# -------------------------------------------------------#
Kind.of.Maybe.instance?(number) # true
Kind.of.Maybe.or_nil(number) # <Kind::Maybe::Some @value=4 ...>
Kind.of.Maybe.instance(number) # <Kind::Maybe::Some @value=4 ...>
Kind.of.Maybe.instance(4) # Kind::Error (4 expected to be a kind of Kind::Maybe::Result)
Kind.of.Maybe[number] # <Kind::Maybe::Some @value=4 ...>
Kind.of.Maybe[4] # Kind::Error (4 expected to be a kind of Kind::Maybe::Result)
The Kind::Optional
constant is an alias for Kind::Maybe
. e.g:
result1 =
Kind::Optional
.new(5)
.map { |value| value * 5 }
.map { |value| value - 10 }
.value_or(0)
puts result1 # 15
# ---
result2 =
Kind::Optional[5]
.then { |value| value * 5 }
.then { |value| value + 10 }
.value_or { 0 }
puts result2 # 35
PS: The Kind.of.Optional
is available to check if some value is a Kind::Optional
.
When you define a method that has default arguments, for certain data types, you will always create a new object in memory. e.g:
def something(params = {})
params.object_id
end
puts something # 70312470300460
puts something # 70312470295800
puts something # 70312470278400
puts something # 70312470273800
So, to avoid an unnecessary allocation in memory, the kind
gem exposes some frozen objects to be used as default values.
Kind::Empty::SET
Kind::Empty::HASH
Kind::Empty::ARRAY
Kind::Empty::STRING
Usage example:
def do_something(value, with_options: Kind::Empty::HASH)
# ...
end
One last thing, if there is no constant declared as Empty, the kind
gem will define Empty
as an alias for Kind::Empty
. Knowing this, the previous example could be written like this:
def do_something(value, with_options: Empty::HASH)
# ...
end
Follows the list of constants, if the alias is available to be created:
Empty::SET
Empty::HASH
Empty::ARRAY
Empty::STRING
After checking out the repo, run bin/setup
to install dependencies. Then, run rake test
to run the tests. You can also run bin/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Bug reports and pull requests are welcome on GitHub at https://github.com/serradura/kind. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the Kind project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.