Last active
March 3, 2023 16:22
-
-
Save kennyfrc/c332be77f10d65e0b48162ac93ddc25a to your computer and use it in GitHub Desktop.
Ruby Example of Railway Oriented Programming
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 'json' | |
## | |
# Combinator module | |
# contains .map, .bind, .apply, .compose | |
# a toolkit for building functional programs | |
## | |
module Combinator | |
Result = Data.define(:success, :error, :value) do | |
def call(&block) | |
if self.success | |
self.value | |
else | |
self.error | |
end | |
end | |
def tee(&block) | |
if self.success | |
p block.call(self.value) | |
end | |
self | |
end | |
def map(&block) | |
if self.success | |
Result.new(true, nil, block.call(self.value)) | |
else | |
self | |
end | |
end | |
def bind(&block) | |
if self.success | |
lambda { Result.new(true, nil, block.call(self.value)) } | |
else | |
Result.new(false, "#{self.error}, line no: #{__FILE__}", nil) | |
end | |
end | |
def apply(&block) | |
if self.success | |
bind { block.call(self.value) }.call | |
else | |
self | |
end | |
end | |
end | |
def self.map(&block) | |
begin | |
Result.new(true, nil, block.call) | |
rescue => e | |
Result.new(false, e, nil) | |
end | |
end | |
def self.bind(&block) | |
lambda { |x| Result.new(true, nil, block.call(x)) } | |
end | |
def self.apply(&block) | |
bind { |x| block.call(x) }.call | |
end | |
def self.compose(method1, method2) | |
lambda { |x| Result.new(true, nil, method1.call(x)).apply { |y| method2.call(y) } } | |
end | |
end | |
## | |
# validate_input, canonicalize_email!, encrypt_email, update_db!, log | |
# these are the methods defined to demonstrate the use of the module | |
# in a web application context | |
## | |
def validate_input(user) | |
if user.email.nil? || user.email.strip.empty? | |
raise "ValidationError: Email is Blank, line no: #{__FILE__}/#{__LINE__}" | |
end | |
if user.name.length > 50 | |
raise "ValidationError: Name is too long, line no: #{__FILE__}/#{__LINE__}" | |
end | |
if user.email !~ /@.*\./ | |
raise "ValidationError: Email is invalid, line no: #{__FILE__}/#{__LINE__}" | |
end | |
if user.name.nil? || user.name.strip.empty? | |
raise "ValidationError: Name is Blank, line no: #{__FILE__}/#{__LINE__}" | |
end | |
user | |
end | |
def canonicalize_email!(user) | |
User.new(user.name, user.email.downcase) | |
end | |
def encrypt_email(user) | |
User.new(user.name, user.email.reverse) | |
end | |
def update_db!(user) | |
# let's pretend there's mutation here | |
"Updating DB with #{user.name} and #{user.email}" | |
end | |
def log(user) | |
"Logging user #{user.name}" | |
end | |
## | |
# Example usage | |
# imagine a web app where the user is created | |
## | |
User = Data.define(:name, :email) | |
user = User.new("John", "[email protected]") | |
result = Combinator | |
.map { validate_input(user) } | |
.map { |user| canonicalize_email!(user) } | |
.tee { |user| update_db!(user) } | |
.tee { |user| log(user) } | |
if result.success | |
json = { "status" => 200, "data" => { "message": "User of name: #{result.value.name} and email: #{result.value.email} created" } }.to_json | |
puts json | |
else | |
json = { "status" => 400, "data" => { "error": result.error } }.to_json | |
puts json | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Based on this: https://www.youtube.com/watch?v=fYo3LN9Vf_M