that allows structuring programs generically while automating away boilerplate code needed by the program logic. Monads achieve this by providing their own data type, which represents a specific form of computation, along with one procedure to wrap values of any basic type within the monad (yielding a monadic value) and another to compose functions that output monadic values (called monadic functions). from Wikipedia https://en.wikipedia.org/wiki/Monad_(functional_programming).
Ruby Syntax sugar for monad Implement monadic syntax in Ruby Examples, DEMO Today, I will not explain mathematics. I will talk about only the programming technique. ここまでで5 〜6 分ぐらいだと良いなあ
specific purpose. is a object that can be mapped by any function. The most popular functor in Ruby is "Array". [1,2,3].map { |i| i.to_s } In Haskell, map for Functor is called fmap .
than two functor objects, But fmap cannot handle more than two functors. [1,2,3].map do |i| [5,6,7].map do |j| i + j end end This sample outputs nested array. Of course, we can use flatten and flat_map . But we have a more functional approach.
{ |pr| pr.call(*extracted) } end.curry(targets.size) applied = targets.inject(pure(curried)) do |pured, t| pured.flat_map do |pr| t.fmap { |v| pr.call(v) } end end applied.flat_map(&:itself) end
express multiple dependent effects. For example, a calculation that may fail depends on whether previous calculations succeeded or failed. In such cases, Monad is useful.
Haskell, Monad requires some implementations. class Monad m where (>>=) :: m a -> (a -> m b) -> m b (>>) :: m a -> m b -> m b return :: a -> m a fail :: String -> m a Especially, (>>=) is most important. it is called "bind operator".
Array, Array#bind receives a function that receives an item contained by the array and outputs new array. Example. ["foo","bar"].bind { |s| s.split(//) } # => ["f","o","o","b","a","r"] In fact, it's flat_map
for { x <- Some(10) y <- functionMaybeFailed() } yield x + y // Return Some(10 + y) or None Scala transforms this code to flat_map style internally. Like below. Some(10).flatMap { x => functionMaybeFailed().flatMap { y => x + y } }
for monad. Haskell has do-syntax, Scala has for-syntax. Because the main purpose of monad is a chain of contextual computation, and syntax sugar is very effective to use it more easily.
do |x| a = x.odd? ? x : x / 2 y <<= [a + 14] z <<= [y, y + 1, y + 2] end end expect(calc.call(7, 8)).to \ eq([21, 22, 23, 18, 19, 20]) This code is valid syntax!! There is no warning.
= a << foo This is valid ruby code. And Ruby treats a as assigned local variable! Moreover, most ruby programmers have not written such codes. In other words, I can change the behavior freely!!
RubyVM::AST.of Detect a pattern like a <<= foo Extract fragments of source code Reconstruct source code Wrap into new proc (to cache reconstructed code) instance_eval new source code ここまでで20 分弱だと良いなあ
block is called Just before evaluating proc's first line proc_binding = tp.binding throw :escape end catch(:escape) do # In Ruby-2.6, TracePoint can limit tracking target. trace.enable(target: block) block.call ensure trace.disable end
ast.children[1] caller_local_variables = proc_binding.local_variables - args_tbl gen = "proc { |#{caller_local_variables.map(&:to_s).join(",")}| ...." I got local_variables and copy into generated proc.
returns Maybe lookup_cache(key1).monadic_eval do |cached_post| cached_comments <<= lookup_cache(key2) # If lookup_cache(key2) fails, # below processes are not executed post = Oj.load(cached_post) comments = Oj.load(cached_comments) post.comments = comments pure post end # return Just(post) or Nothing
<<= balance_a.ensure_amount!(amount) # return Either balance_b <<= either { Balance.find_by!(user: user_b) } TransferMoneyService.new(from: user_a, to: user_b, amount: amount) .process # return Either end # return Right(result) or Left(exception) Either hides error handling behind monad syntax. And it has a high affinity with pattern matching. Pattern matching is very hot topic.
: x / 2 b = Concurrent::Promises.future { sleep 2; 11 } c = Concurrent::Promises.future { sleep 1; 3 } d <<= b # wait b e <<= c # wait c pure(a + d + e) end # Return Future(19) like async syntax of JavaScript.
and outputs value and next state. class State include Monad # @param next_state [Proc (s -> [a, s])] def initialize(next_state) raise ArgumentError.new("need to respond to :call") unless next_state.respond_to?(:call) @next_state = next_state end end
= run_state(s0) new_state_monad = pr0.call(x) new_state_monad.next_state.call(s1) end ) end run_state(s0) -> [a, s2] -> block.call(a) -> new state monad -> run_state(s1) These processes are wrapped by the new State class.
# Return initial state _ <<= if status != :saved result = n * 10 put(:saved) # Update state else pure(nil) end pure(result) end val, st = state.run_state(:not_saved) # val == 50, st == :saved val, st = state.run_state(:saved) # val == nil, st == :saved
and returns [[Object, String]] . Object is a result of parsing. String is remained characters. flat_map expresses parser combination. | expresses selective parser.
# return Array result0.flat_map do |output, remained| next_parser = pr.call(output) next_parser.run_parser(remained) end end ) end Below processes is wrapped by Proc . Get result -> given proc processes result -> Get New Parser -> parse remained chars.
monad is very powerful Syntax is very important I want other representations for nested block By this implementation, I recognized the fun of monadic programming again.