TL;DR Describing typical problems with ruby function arguments. How I solved them with a gem. What other options are.
Ruby syntax
keyword arguments
Ruby 2.0 introduced first-class support for keyword arguments:
def foo(bar: 'default')
puts bar
end
foo # => 'default'
foo(bar: 'baz') # => 'baz'
In Ruby 1.9, we could do something similar using a single Hash
parameter:
def foo(options = {})
bar = options.fetch(:bar, 'default')
puts bar
end
foo # => 'default'
foo(bar: 'baz') # => 'baz'
Ruby 2.1 introduced required keyword arguments, which are defined with a trailing colon:
def foo(bar:)
puts bar
end
foo # => ArgumentError: missing keyword: bar
foo(bar: 'baz') # => 'baz'
foo(bar: 'baz', baz: 'baz') # => ArgumentError: unknown keyword: baz
Optional named arguments
def foo(bar:, baz: nil)
puts bar, baz
end
foo(bar: 'baz', baz: 'baz') # => 'baz' 'baz'
def foo(bar:, **args)
puts bar, args.fetch(:baz)
end
foo(bar: 'baz', baz: 'baz', other: 'other') # => 'baz' 'baz'
No way to distinguish between nil
and no value
Assume we have method similar to ActiveRecords’ where
, so it can accept different parameters to filter results.
def where(name: nil)
if name.nil?
# filter
else
# all results
end
end
where() # all records
bar = 'baz'
where(name: bar) # only records with name == 'baz'
bar = nil
where(name: bar) # expect records with name == nil, but instead it will give all records
Solution:
def where(options = {})
if options.key?(:name)
# filter
else
# all results
end
end
# Ruby 2.0 syntax
def where(**options)
options = options || {}
if options.key?(:name)
# filter
else
# all results
end
end
No way to check allowed parameters to prevent typos
Take a look at code from previous section at different angle
def where(name: nil)
end
where(nane: nil) # ArgumentError: unknown keyword: nane
def where(options = {})
end
where(nane: nil) # expect ArgumentError, but instead it will give all records
Solution:
def where(options = {})
allowed_arguments = [:name]
extra_arguments = options.keys - allowed_arguments
raise ArgumentError.new("unknown keyword: #{extra_arguments[0]}") if extra_arguments.length > 0
if options.key?(:name)
# filter
else
# all results
end
end
where(nane: nil) # ArgumentError: unknown keyword: nane
No way to do only one optional parameter
Assume you have really broad database of products. It includes products with different types of SKU numbers. There is no sense to search by more than one identifier: where(upc: '123', ean: '456')
Solution:
def where(options = {})
raise ArgumentError.new("One and only one keyword required") if options.length != 1
allowed_arguments = [:upc, :ean, :isbn]
extra_arguments = options.keys - allowed_arguments
raise ArgumentError.new("unknown keyword: #{extra_arguments[0]}") if extra_arguments.length > 0
if options.key?(:upc)
# filter
else
# all results
end
end
Some code snippets taken from thoughtbout article.
Gem
I extracted all those snippets into strong_arguments gem and added some DSL inspired by strong_parameters. Work in progress though.
Usage
require 'strong_arguments'
def where(options = {})
arguments = StrongArguments.new(options)
.optional(:name, :age)
if argument.name_present?
argument.name
elsif argument.age_present?
argument.age
end
end
Notice: work in progress. Examples below may not work.
Types
It is possible to add type checking in manner of stronger_parameters.
def where(options = {})
arguments = StrongArguments.new(options)
.optional(name: StrongArguments.string, age: StrongArguments.integer)
end
Defaults
It is possible to add defaults. Example of how it can look like
def where(options = {})
arguments = StrongArguments.new(options)
.optional(name: StrongArguments.string.default('Joe'), age: StrongArguments.integer.default(10))
end
Other view to the problem: method signatures DSL
sig
sig adds the sig
method that allows you to add signatures to Ruby methods.
sig [:to_i, :to_i], Integer
def sum(a, b)
a.to_i + b.to_i
end
sum(42, false) # Sig::ArgumentTypeError: Expected false to respond to :to_i
This solution introduce gradual and structural type system.
See also:
- Gradual Type Checking for Ruby
- Gradual Typing Bibliography
- An open letter to Matz on Ruby type systems
Rubype
Rubype brings you advantage of type without changing existing code’s behavior.
require 'rubype'
class MyClass
def sum(x, y)
x + y
end
typesig :sum, [Numeric, Numeric] => Numeric
end
MyClass.new.sum(1, 'string') # Rubype::ArgumentTypeError: for MyClass#sum's 2nd argument
RTC
Rtc Ruby Type Checker
Looking into the future
Static type checker
Interestingly, Matz suggested that this kind of system would initially be implemented as a static analyzer, and that it would require new restrictions on some common Ruby methods, such as require and methods_missing, in order to support a type-checked universe. It isn’t totally clear what those restrictions would need to be, but it’s a fun thought experiment.
Ruby static type analyzer can be implemented the same way as flow for JavaScript.
See also:
- issue 9999
- https://codon.com/consider-static-typing
- https://www.cs.umd.edu/projects/PL/druby/papers/druby-oops09.pdf
- https://blog.abevoelker.com/sick-of-ruby-dynamic-typing-side-effects-object-oriented-programming/
Auto-generated documentation
Method signatures will allow to automatically generate documentation for methods
desc 'Sums two values'
sig [:to_i, :to_i], Integer
def sum(a, b)
a.to_i + b.to_i
end
Similar thing expressed in YARD
# Sums two values
#
# @param a [Integer] first value
# @param b [Integer] second value
# @return [Integer]
Or mixed with YARD
# Sums two values
#
# @author Loren Segal
# @deprecated Use `+` instead
sig [:to_i, :to_i], Integer
def sum(a, b)
a.to_i + b.to_i
end
Enforced semantic visioning
Idea comes from elm. See also: semantic versioning.