Skip to content

Instantly share code, notes, and snippets.

@bdragon
Last active November 6, 2019 20:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bdragon/ef8f9f364d14d341e6ddebc51b69819e to your computer and use it in GitHub Desktop.
Save bdragon/ef8f9f364d14d341e6ddebc51b69819e to your computer and use it in GitHub Desktop.
# == Library code ===========================================
class Result
def self.success(value)
new(nil, value)
end
def self.error(err)
new(err, nil)
end
attr_reader :error, :value
def initialize(error, value)
@error, @value = error, value
end
def ==(other)
self.class == other.class && error == other.error && value == other.value
end
alias eql? ==
def error?
!error.nil?
end
def success?
error.nil?
end
def flat_map
(yield value) if success?
self
end
def on_error
(yield error) if error?
self
end
end
# == Monad Laws =============================================
# trait Monad[F[_]] {
# def pure[A](a: A): F[A] = ???
# def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B] = ???
# }
# 1. Left identity: pure(a).flatMap(f) == f(a)
# 2. Right identity: m.flatMap(pure) == m
# 3. Associativity: m.flatMap(f).flatMap(g) == m.flatMap(a => f(a).flatMap(g))
# (A * B) * C == A * (B * C)
# pure[A](a: A): F[A]
pure = Result.method(:success)
# f: A => F[B]
square = proc { |x| Result.success(x * x) }
minus1 = proc { |x| Result.success(x - 1) }
# 1. Left identity: pure(a).flatMap(f) == f(a)
puts "Left identity: #{ pure.call(2).flat_map(&square) == square.call(2) }"
# 2. Right identity: m.flatMap(pure) == m
puts "Right identity: #{ pure.call(2).flat_map(&pure) == pure.call(2) }"
# 3. Associativity: m.flatMap(f).flatMap(g) == m.flatMap(a => f(a).flatMap(g))
# (A * B) * C == A * (B * C)
puts "Associativity: #{ pure.call(2).flat_map(&square).flat_map(&minus1) == pure.call(2).flat_map { |a| square.call(a).flat_map(&minus1) } }\n"
# == Domain code ============================================
class User
def self.find_by(id:)
new(id: id)
end
attr_accessor :id
def initialize(id:)
self.id = id
end
end
class Project
def self.find_by(id:)
new(id: id)
end
attr_accessor :id
def initialize(id:)
self.id = id
end
end
class Report
def initialize(user, project)
@user, @project = user, project
end
def to_s
"report for project #{@project.id} generated by user #{@user.id}"
end
end
# == Application code =======================================
module FindUser
def self.call(id)
user = User.find_by(id: id)
user ? Result.success(user) : Result.error("user not found")
end
end
module FindProject
def self.call(user, id)
project = Project.find_by(id: id) # user.projects.find_by(project_id: id)
project ? Result.success(project) : Result.error("project not found")
end
end
module AuthorizeUser
def self.call(user, resource, scope)
authorized = true
authorized ? Result.success(true) : Result.error("user not authorized")
end
end
module BuildReport
def self.call(user, project)
report = Report.new(user, project)
Result.success(report)
end
end
module RenderReport
def self.call(report, format)
case format
when :string
Result.success(report.to_s)
else
Result.error("unsupported format, #{format}")
end
end
end
module Program
def self.run(user_id:, project_id:)
user, project = nil, nil
FindUser.call(user_id) # Result[User]
.flat_map { |u| user = u; FindProject.call(user, project_id) } # Result[Project]
.flat_map { |p| project = p; AuthorizeUser.call(user, project, :show?) } # Result[Boolean]
.flat_map { |*| BuildReport.call(user, project) } # Result[Report]
.flat_map { |r| RenderReport.call(r, :string) } # Result[String]
.on_error { |e| raise e }
.value # String
end
end
output = Program.run(user_id: "10", project_id: "4")
puts "Result: #{output.inspect}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment