Skip to content

Instantly share code, notes, and snippets.

@baweaver
Created February 14, 2018 07:50
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save baweaver/611389c41c9005d025fb8e55448bf5f5 to your computer and use it in GitHub Desktop.
Save baweaver/611389c41c9005d025fb8e55448bf5f5 to your computer and use it in GitHub Desktop.
Having fun with M and Q
# A few fun tricks right quick. Note that more tricks like this are over at:
# https://medium.com/rubyinside/triple-equals-black-magic-d934936a6379
#
# I'll probably condense this into a blog post later, but for now we'll have our fun.
# 1 - "Pattern Matching" with case
# We make a new lambda named M for brevity. Because it can be called with `[]` it
# looks quite natural in flow with a case statement.
#
# Now then, we form a closure around our provided matchers, all of which are assumed
# to be objects which respond to `===`, some in more interesting ways than others.
#
# For those who don't know what a closure is, it's what happens when a lambda is initialized.
# It remembers the context of its initialization, meaning our "other" lambda returned from M
# "remembers" what matchers are.
#
# One might notice the `:*`, which is symbolic of a match-all
M = -> *matchers {
-> other {
matchers.each_with_index.all? { |m, i| m === other[i] || m == :* }
}
}
# Let's give it a quick whirl shall we?
# 1.1 - Regex match: Think tuples
case ['Bob', 25]
when M[/^B/, :*] then "It's Bob!"
else "Well, guess not."
end
# 1.2 - Type match: Let's say we want to respond differently to different signatures, like a dispatch
case ['10.0.0.1', 15]
when M[String, Integer] then "It's an IP hit count"
when M[String, Array] then "It's a group of IP logs"
else "Dunno"
end
case ['10.0.0.1', %w(log log log log log)]
when M[String, Integer] then "It's an IP hit count"
when M[String, Array] then "It's a group of IP logs"
else "Dunno"
end
# 1.3 - Lambda match: How about we go a bit further? Scala does something like this
greater_than = -> a { -> b { b > a } }
case ['Jaime', 24]
when M[:*, greater_than[25]] then "Can be as old as Ruby"
when M[:*, greater_than[20]] then "Can drink"
when M[:*, greater_than[17]] then "Legal adult"
else "Too young to tell"
end
# Closure lambdas can be incredibly incredibly useful for some applications.
#
# If you want to take it to an extreme: https://github.com/lazebny/ramda-ruby
#
# Read this as well: http://randycoulman.com/blog/2016/05/24/thinking-in-ramda-getting-started/
#
# ...ah, but we're not done quite yet. I said tricks and tricks we shall have! Enter "Q"
# 2 - "Query Matching" with case
Q = -> **keyword_matchers {
-> other {
keyword_matchers.all? { |key, matcher| matcher === other[key] }
}
}
Q[name: /^B/] === {name: 'Brandon'}
# Let's get us some JSON to play with. JSON is fun!
require 'json'
require 'net/http'
posts = JSON.parse(
Net::HTTP.get(URI("https://jsonplaceholder.typicode.com/posts")),symbolize_names: true
)
# 2.1 - A "select" sort of query
#
# I don't know about you, but I rather dislike writing this:
posts.select { |post| post[:userId] == 1 }
# How about Q?
posts.select(&Q[userId: 1])
# Though don't take my word for it:
posts.select(&Q[userId: 1]) == posts.select { |post| post[:userId] == 1 }
# If you were _really_ feeling a bit evil, you _could_ switch `other[key]` to
# something like `other.public_send(key)` :D
Q2 = -> **keyword_matchers {
-> other {
keyword_matchers.all? { |key, matcher| matcher === other.public_send(key) }
}
}
require 'ostruct' # because lazy admittedly
post_objects = posts.map { |post| OpenStruct.new(post) }
post_objects.select(&Q2[userId: 2])
# Now noted that Q doesn't deal with nested hashes. Could it? Sure, but that'd be some fun
# writing I leave as an exercise to the unfortunate reader.
#
# Happy hacking!
# - baweaver
@baweaver
Copy link
Author

baweaver commented Feb 14, 2018

PS: If you want something a bit more dignified than random lambdas for M and Q, you can abuse self.[] for new classes:

class M
  def initialize(*matchers)
    @matchers = matchers
  end

  def self.[](*ms) new(*ms) end

  def ===(other)
    @matchers.each_with_index.all? { |m, i| m === other[i] || m == :* }
  end
end

@baweaver
Copy link
Author

It should be noted that you really probably want Qo instead if you actually want to use these ideas:

https://www.github.com/baweaver/qo

Qo is effectively the result of this gist taken to extremes in a more succinct Ruby-like API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment