I was in Chicago this week. I was given the opportunity to attend a conference this fall, and I picked RubyConf'24. Since taking a new job in July, I am back developing code part of my time. My entire stint with NextEra Energy (over five years), I did not actively develop code that was destine for a production environment. That was one of the Faustian Bargains I had to make when accepting that job in 2019: I would not be writing software, I would be dedicating my time to managing people and projects. Prior to NextEra, I was at the Institute for Social Research and Data Innovation, and it was a Ruby shop. I was hired there in 2012 as a senior Ruby developer. And that's what I primarily did: write Ruby code. NextEra Analytics was a Kotlin/NodeJS/Python shop; there was no place for Ruby.
But, I am back, actively looking through and developing software, in Ruby, no less. So, on Wednesday of this week, I found myself sitting in the audience at Hilton Chicago, listening and watching the creator of the Ruby language, Yukihiro Matsumoto give the keynote. It was a broad based overview of Ruby, where it came from, highlights over the years and a bit of a vision on where Mats sees it going. I had forgotten how much I like writing Ruby. It is a real joy.
So, I took a little time to write some Ruby. I settled on a topic that I wrote extensively about earlier in the year: pricing stock options. Most of that work was done in Rust and I investigated different numerical methods for pricing options. I did not go that far with my Ruby, in fact, I settled on implementing Black-Scholes, which, in its basic form, is only useful for pricing European-style options. Black-Scholes does not account for the early exercise American-style options.
# frozen_string_literal: true module BlackScholes module_function OptionType = Struct.new(:name) CALL = OptionType.new(:call) PUT = OptionType.new(:put) def calculate(type:, stock_price:, strike_price:, time_to_expiry:, risk_free_rate:, volatility:) params = { type: type, stock_price: stock_price, strike_price: strike_price, time_to_expiry: time_to_expiry, risk_free_rate: risk_free_rate, volatility: volatility } validate_params(params) d1, d2 = calculate_d1_d2(stock_price, strike_price, time_to_expiry, risk_free_rate, volatility) discount_factor = Math.exp(-risk_free_rate * time_to_expiry) option_price = case type when CALL stock_price * norm_cdf(d1) - strike_price * discount_factor * norm_cdf(d2) when PUT strike_price * discount_factor * norm_cdf(-d2) - stock_price * norm_cdf(-d1) else raise ArgumentError, "Invalid option type: #{type}" end option_price.round(6) end def option_calculator_for(type) ->(params) { calculate(type: type, **params) } end def calculate_d1_d2(s, k, t, r, sigma) components = [ -> { Math.log(s / k) }, -> { r * t }, -> { (sigma**2 / 2) * t }, -> { sigma * Math.sqrt(t) } ] d1 = components.take(3).sum(&:call) / components.last.call d2 = d1 - components.last.call [d1, d2] end def norm_cdf(x) (1 + Math.erf(x / Math.sqrt(2))) / 2 end def validate_params(params) required_params = %i[type stock_price strike_price time_to_expiry risk_free_rate volatility] numeric_params = required_params - [:type] missing_params = required_params - params.keys raise ArgumentError, "Missing parameters: #{missing_params.join(', ')}" unless missing_params.empty? params.each do |key, value| case [key, value] in [:type, OptionType] next in [param, Numeric => n] if numeric_params.include?(param) raise ArgumentError, "#{param} must be positive" unless n.positive? in [param, _] if numeric_params.include?(param) raise ArgumentError, "#{param} must be a positive number" else raise ArgumentError, "Invalid parameter: #{key}" end end end end # Usage example option_params = { stock_price: 100, strike_price: 100, time_to_expiry: 1, risk_free_rate: 0.05, volatility: 0.2 } [BlackScholes::CALL, BlackScholes::PUT].each do |option_type| price = BlackScholes.calculate(type: option_type, **option_params) puts "#{option_type.name.capitalize} Option Price: #{price}" end
(base) alex:~/projects/ruby-3.3.6-bin$ ./bin/ruby bs3.rb Call Option Price: 10.450584 Put Option Price: 5.573526
Here are some of Ruby's most notable and unique features:
1. Everything is an Object
-
True: In Ruby, every entity (including primitives like numbers, booleans,
nil
, and even classes) is an object. This means all these entities have methods and can respond to messages. For example, you can call theto_s
method on any object to get its string representation.
5.to_s # Returns "5" nil.to_s # Returns ""
2. Anonymous Functions (Blocks, Procs, and Lambdas)
- Supported: Ruby offers several forms of anonymous functions, each with slightly different behaviors:
-
Blocks: Not objects by themselves but can be converted to
Proc
objects. They are defined withdo...end
or{...}
and are commonly used with methods that yield.
[1, 2, 3].each do |element| puts element end
-
Procs: Are objects and can be defined using
Proc.new
or theproc
keyword. They have lenient argument handling (ignoring extra args).
my_proc = proc { |a, b| puts a + b } my_proc.call(2, 3) # Outputs: 5
-
Lambdas: Also objects, defined with the
->
syntax orlambda
keyword. They enforce argument counts strictly.
my_lambda = ->(a, b) { puts a + b } my_lambda.call(2, 3) # Outputs: 5
3. Open Classes
- Ruby allows you to reopen and modify classes even after they've been defined. This is useful for adding functionality to core classes or third-party libraries.
class String def greet "Hello, #{self}!" end end "World".greet # Returns "Hello, World!"
This is sometimes referred to as monkey patching.
4. Metaprogramming
- Ruby's syntax and nature make it extremely conducive to metaprogramming techniques, allowing for dynamic creation and modification of code at runtime.
5. Dynamic Typing
- Variables in Ruby do not have explicit types. The data type of a variable is determined at runtime, making the language very flexible but also potentially error-prone if not managed carefully.
6. Modules (Mixins)
- Modules are similar to classes but cannot be instantiated. They're primarily used for namespacing and as mixins to provide a way to compose a class's behavior from multiple sources.
module Greetable def greet "Hello!" end end class Person include Greetable end Person.new.greet # Returns "Hello!"