This post by Evan Hahn shows how static typing in Crystal can result in removing some validations you might need in Ruby and thus static types can be a way shorten code.
Sure, sometimes! But, his example of Ruby code that could be shortened leaves some room for improvement. Ruby can be used to create rich and powerful data types, even if they aren't checked by a compiler.
Here's his code (formatted to not be so wide):
def initialize(min_length)
unless min_length.is_a?(Integer) &&
min_length >= 0 &&
min_length <= 255
raise "Minimum length has to be between 0 and 255"
end
@min_length = min_length
end
Ignore the is_a?(Integer)
for a second and focus on the validation that min_length
must be between 0 and 255. His Crystal code requires specifying the type, so he used the builtin type UInt8
. This is an unsigned 8-bit value, so by definition cannot be outside the range of [0,255].
Ruby doesn't have this particular type, but we could certainly make it!
class UInt8
def initialize(value)
unless (0..255).include?(value)
raise ArgumentError,
"#{value} must be between 0 and 255"
end
@value = value
end
def to_i = @value
end
We don't require that value
be an Integer
since the range check implies that.
UInt8
is now a clearly defined datatype, which means that the original Ruby code could be:
def initialize(min_length_unit8)
@min_length = min_length_uint8
end
I suffixed the value with the type name, as is customary. And sure, you could check that min_length_uint8
is a UInt8
if it were important, but it often isn't worth checking. It depends on how the value is used.
I do all this not to say that the Ruby version is equivalent to the Crystal version, but just that you can certainly define types in Ruby however you like. Just because Ruby doesn't have a built-in type for an 8-bit unsigned integer, doesn't mean you can't make one and use it.
But what it does mean is that by defining types you can write code that doesn't have to check if the primitive being passed is valid. Wrap the primitive in a type that does the checking!
Integer
While our UInt8
can be coerced into an Integer
via to_i
, we could use SimpleDelegator
to make it behave much more like one.
A class that extends SimpleDelegator
passes an object to the constructor via super(...)
. Any method call on an instance of that class gets delegated to the wrapped object.
require 'delegate'
class UInt8 < SimpleDelegator
def initialize(value)
unless (0..255).include?(value)
raise ArgumentError,
"#{value} must be between 0 and 255"
end
super(value)
end
end
x = UInt8.new(42)
puts x + 10 # => 52
puts 20.0 / x # => 0.47619047619047616
This demonstrates one reason why Rubyists don't check the type of something. In this case, UInt8
may not by a subclass of Integer
, but it behaves like one. This is the difference between nominal typing (types based names assigned to them, like UInt8
) and structural typing (types based on the structure of the object, e.g. what methods the object responds to, also called duck typing).
Next time you are messing about with validating strings or hashes, try making a data type instead, and passing that to your core logic. By insulating your core logic with well-defined types, you can simplify the most complex part of your app…and you don't need a type-checking compiler to do it!
It's hard to keep up with everything, and sometimes I learn something that has been common knowledge for a while. Maybe this happens to you.
JavaScript's' arrow functions don't work like a function declared with function
. I had previously thought arrow functions were just syntactic sugar. They aren't, especially when it comes to this
In JavaScript, the value of this
can be complicated. When you declare a function with function
, you can later change what this
inside that function refers to:
class Functions {
constructor() {
this.foo = "foo!"
this.method = function() { return this.foo }
}
}
const f = new Functions()
const x = f.method
console.log(x())
// TypeError: Cannot read properties of undefined
console.log(x.bind(f)()) // => "foo!"
console.log(x.bind({ foo: "bar" })()) // => "bar"
console.log(x.bind({ foo: 42 })()) // => 42
With arrow functions, you can'd use bind
this way. Well, you can, it just doesn't have any effect:
class Functions {
constructor() {
this.foo = "foo!"
this.method = () => { return this.foo }
}
}
const f = new Functions()
const y = f.method
console.log(y()) // => "foo!"
console.log(y.bind(f)()) // => "foo!"
console.log(y.bind({ foo: "bar" })()) // => "foo!"
console.log(y.bind({ foo: 42 })()) // => "foo!"
I found this behavior subtle, especially since bind
was able to be called without an error. But…today I learned!
Unless otherwise noted, my emails were written entirely by me without any assistance from a generative AI.