you
yes sorry so let's talk about case which is a keyword that hopefully most of you have
used, if you haven't, it's okay, we're gonna go through it. And we're gonna figure out
how we can use it, how it works, how we can use it better, and what the latest
versions of Ruby have given us to play with this operator more. So yeah, that's
more as what I'm talking about. So just in case we're gonna go through what case
is, what the different syntaxes are, how you usually use it, and then we're gonna
look at how it's implemented, which is terrifying, and we're gonna have a small
dive into how the Ruby VM works and the instructions and stuff like that. After
that we're gonna go through several use cases, some of them are pretty basic, some
of them I think are pretty cool on a Ruby standpoint. And finally we're gonna
take a look at pattern matching, which has been coming to Ruby since 2.7 and
is mainly operated right now using the case keyword. So let's start. What's a
case? So does anyone not know what a case is, or has anyone not used it? Cool. So
that will go fast. So basically a case is more or less a big if-else, that's
usually how people think about it. So you have your case, you have your different
branches, and then you match each branch against your case. And depending on the
branch that matches, you go to a different path. So in this case we can assume
that, I don't know, status is something you get back from an API, you match it
against different cases, and then if you have a success you proceed, otherwise you
want to fail depending on what you have. If you want to, you could be even more
compact by moving the stuff up a line and using then, and if you want it to be
even more compact, you could even add more things to a branch. So if you wanted
different conditions to go to the same branch, you can separate them with a
comma. So that's basic case. One interesting use case that I don't think I've
ever seen before, I don't know if it's useful, but it's still cool to look at,
is you can write a case without anything at the top, just an empty case, and then
it behaves exactly like an if-else if, so you have to use usual predicates,
the same way you would an if-else. So I'm honestly not sure that has much
interest, but it's cool. So how does case work? And in general, I kind of also
wanted to take the opportunity to talk about a bit about how anything works in
Ruby and how you can, when you're debugging something and trying to figure
out how something works, how you can go deeper into your code or someone else's
code. So if you're, for example, if you have a method that you've written or
someone else has written and you don't know where it is, so let's say you're in
a big code base and you have 20 methods called, I don't know, count or show, and
you don't know which one is being resolved. In Ruby, everything's an object,
as you might have heard before. So, so are methods. And you can, on any instance
of anything, call dot method, capture your method, and then you have access to
two methods that are pretty cool. One is called source location, which will tell
you in which file it is. So interesting when you don't know which method is
being resolved. And another one is just dot source, which will print out the
source in your terminal. Just plain up. So that's interesting also. If you're
looking for something more lower level, so a Ruby method, like array.last or
integer.next, and you don't know how it works and you don't know where to go,
you're kind of stuck. You're going to have to go read the fabulous manual of
Ruby to figure out where it is. But in our case, we're kind of one level deeper
because we're not looking at a Ruby method, we're looking at a Ruby keyword. So
you, if you go to the documentation, you're going to find how it behaves, but
you're not really going to be able to see the source code per se. So in this case,
one way that I've used to figure out how the internals of case work is to go
look at the Ruby VM instructions. So big-ish caveat for the next couple of
slides. That's the very limit of what I'm trying to understand this year. I'm
kind of in that phase in my Ruby journey when I want to understand how things
work. So if I say something outrageous, stop me. So from my understanding, the
Ruby code that you write goes through a journey before it is compiled and
interpreted. So your Ruby code first gets turned into tokens. So for example, you
can imagine that your entire program gets turned into a big array of syntactically
relevant stuff. So that could be depth, for example, or an open parenthesis or a
space or part of a string. So everything gets turned into a token. And then those
token get organized into something called an AST, which is an abstract syntax
tree, which is really hard to say. And basically what an AST is, is that big
array, but formatted into something that is more understandable. So if anyone has
ever played with RuboCop before, that's probably where you've seen something
like that, because you have to play with syntax tree, which you want to write your
own cops. So the tree is composed of a lot of nodes, and each node has a name. So
you have a class node or a method node or a begin node. And then inside the node,
you have all relevant information for that specific class or method or begin
block or anything. And then all of those, all that tree gets turned into virtual
machine instructions. So that's the part where I think this, what I'm going to
talk about probably only work on C Ruby. I'm not sure this applies to other
implementations of Ruby, like Truffle or JRuby. It probably works a bit
differently. So if we look at the case that we're looking at before, and in the
Ruby console, you have a class called RubyVM, which gives you access to any tool
you might want to, to turn your code into either the tokens or the tree or the
instructions. You can end up with all of this, which we're going to try and go
through in some kind. So first of all, in case you've never used it, the Ruby
virtual machine, the one from C Ruby is a stack based VM. So interact, everything
in the VM is a stack. So you end up, you have a lot of instruction here that just
interact with the stack. Like the put object over there just puts an object on
the stack, the top end finds an object and then moves it to the top of the stack.
And you have a lot of things like that. So in our case, if we look in detail, we
can see a few things. So first of all, here we're mainly preparing the stack and
here we have something, here we can find the status that we have over there. So
this is basically calling status to fetch the value that we want to match
against. And under this, you have a Ruby optimization, a Ruby VM optimization
called case dispatch. What this does is in some cases, if you're using a simple
case with simple objects inside of it, like strings or integers or symbols or
stuff like that, what it will do is it will create a hash where the keys are
basically this and this and the values are the number of the line in your VM
structure that you need to jump to. So what that means, at least the way I
understand it, is if you have a lot of if, else if, else if, else if, it will be
usually faster to build a case because you're losing some time here to build
your hash. But then whatever case you want to go to, it's just a hash access.
Whereas if you're doing a bunch of if and else if you have to go through each of
them to see does this work or does this work or does this work, etc.
If we go a bit below, we can see what that would look like technically if we
would need it to go through each of the branches to see which one works. So here
we have our success symbol, which was our first branch. And what this does is it
going to compare it to the status using the triple equal method. And that's the
cool part of case. That's technically what's doing the heavy lifting behind.
And if that equal works, then it's going to jump to instruction 28 below. If it
doesn't work, then it's going to keep going. So second branch is error. So we're
going to take error, put it on the stack, compare it to status. And if it doesn't,
if it works, we go to 33. If none of those work, if you remember the case,
then that means we're in our error case or like our else, which is over there. So if
none of those work, so we keep going down our instruction and we end up here
called the fail harder and then leave, which is instruction 28. And then under that,
then you have the lines that you would have jumped to if anything worked before. So the
28 here, which will call the proceed and the 33, which will call the fail. So that's
more or less the instruction patterns of a case. So that turned our question before,
answers our question before, right, of how a case works. And the simplest answer that I can
give it, it works thanks to triple equal. That's what it's going to use to match everything
against everything. So if we wanted to push case to the limit, the question that we want to answer
now is what does implement triple equal? And in Ruby, that's a bunch of classes. And the
interesting thing and the main reason I wanted to do that presentation is that depending on what
you're calling triple equal on, it will behave differently. So the simplest example that we've
all used is all the base classes. So string strings, integers, float, arrays, hashes,
anything you want. And in this case, it checks for equality. So that's the thing we've seen
before. You might have seen that code. You get a param that has a response and then you don't
know what the fuck the other person in the API has done, whether it's a string or a 200 or a
success or a string or a true or true as a string or anything. So you do your case and you match it
against whatever and try to figure out. So in this case, it's always going to check for equality.
So here with the come out that we've seen before, it's one or the other or the other.
And then you have arrays, you have hashes. Otherwise, yes, you can give up. Another thing
that implements triple equal with another behavior are classes and modules. On classes and on modules,
triple equal checks for, I don't really know how to say it in one word, checks for type,
for ancestry. It's a bit like the is a method of Ruby. So when you have an object and you call is
my dog an animal, it's not only going to check the class, it's going to check a bit above to see if
animal is included in it if you're going composition way or if it inherits from animal, if you're
going the inheritance way. And that's more or less what we can do here, for example, with errors.
So I say you have your code and you've defined a bunch of different types of errors. And you've
tagged some of them maybe as ignorable. So if it returns any errors that's in that type, then I
want to ignore them. If it returns those two different errors, I want to return a not found.
If someone forgot about safe navigation, I want to tell them. And then a lot of errors, for example,
in Rails, and I'm assuming in Ruby, not entirely sure, don't put me on that,
inherit from standard error. And so those maybe you want to raise, but if you have something else,
then that's probably a lower level, maybe a PG error if you're dealing with a database,
and then you want to do something else. So that's it for classes and modules. Another
class, another type of classes that implement triple equal or ranges that I'm assuming most of
most of us have already used that check for inclusion. So for example, if you have
an integer at the top, then you can check that it's included in this range or this range. And it
works with the endless ranges of Ruby. So you can be very, I mean, this might as well use an if
else if and just check that it's greater or lower than, but it's good to have options.
You never know. And one thing that I found, if you're working in networking, that could be cool,
IP address works the exact same way. So you can define IP addresses with their masks and everything,
and then have them act as ranges, and then check that your IP address belongs to one or the other.
This one we've all probably used is also is reg X. So this one checks for just a match. It's the
exact same equivalent as if you wanted to match your against string. So that's a kind of real
use case that I have from the company that I'm working for where we manage a lot of messages
between clients and providers. And so we want to check in those messages that they're not trying
to bypass us, for example, by sending an address and trying to meet somewhere, or they're not
sending sensitive information or sometimes people can keep their dick in their pants. So we have to
be careful about that also. Stuff like this, right? So this one checks for match. Probably one of the
most interesting example, but yet the one that have the most trouble coming up with a good example
for are prox and lambdas. On prox and on lambdas, triple equal calls the lambda and gives it the
object that you're matching with. So for example, here we can define, let's say we want to define
simple prox or lambdas that just delegate to another method. So for example, unknown host will take
an element and then check if the host is included in the list of something.
Oh shit, yeah, I've done it again. In case this is just, it's the new way of writing the old thing
here with the pipe pipe and you enter a variable, this does the exact same thing. It just takes the
first one. So underscore one would be the first variable that you enter here, underscore two,
the second one, underscore three, et cetera, et cetera, et cetera. So let's say that we've defined
a simple list of hosts. So when we get, in this case, probably a request, we could delegate to
one of those to see if it whitelisted or if something went wrong. And then we can, if it goes
there, yes, we can take a request, let's say a web book for example, and write our case on it and say,
okay, when it's whitelisted, then I want to do something. If the host is unknown, I want to
do something else. If the action is unknown, it's going to do something else. And what this is going
to do behind the curtain is it's going to call whitelisted and give it web book as a first parameter.
So it's a more, again, more compact way and allows you to put that code somewhere else
instead of having to copy paste it into three ifs. And the last one, we're in Ruby, thankfully.
So for every other class, we got duck typing. We can just implement the triple equal method and
have it work for more or less anything that we want. So bear with me because that's going to take
a little bit of time. So in this case, same, still sticking with my response example that we've
been following the entire presentation. So here I can define in my response class or module or
whatever different classes that implement the triple equal and that do anything that I want.
And then I can, if I do this and I call them, this is going to do what we've seen before in the
VM instructions, right? It's going to take the response called triple equal with this and then
see if the answer is true or not. So with this, you can basically create as many matches as you
want, especially on custom class that can be pretty interesting. If you have one example that came
to mind also is payments, for example, if you're managing payments, then you can in your payment
class define different subclasses that could be success or canceled or processing that just
calls your payment API and checks if it works. And so all that code is it's in own place and then
you instantiate your object here and you can use case to easily delegate where you're going.
Another example that we've kind of used is a wrapper for services. So basically you define
new classes for your service and your service answer a class that's either a success or an error
and then you can use this to do some kind of early, early days pattern matching.
So speaking of pattern matching, how does it work? So just in, again, just in case,
we're going to go quickly through what it is and what it works, how it works, sorry.
So the whole idea of pattern matching is that you define as the naming price, you define a pattern,
then you try and match it against something and see what sticks. So here my pattern is going to be a
hash with a status key, a body key inside of which I'll have a user with a name and an age
and whatever is in here, if I can match it, I want to store it in the variable and then once you
have your pattern, you can try and match it against any collection of stuff. So in this case,
it's going to work because we had the same status and the form that we're trying to match against
was the same and what it's going to do is it's going to assign the name variable to whatever was
there and the age variable to whatever was there. If you want to match it against something that
looks very different, so this hash for example is not going to work because either status and body
are here, this value is not going to match against that one, right? So if you try and do this, then
it doesn't work, so you're going to get an error. In Ruby at least, this is going to, sorry, this is
going to raise an error that just tells you I wasn't able to match it and in Ruby that was implemented
using case. So the way it works is if you have a response or literally anything, you want to create
your different patterns that you're going to want to match it against and one thing to note is that
it's no longer, you know, to make the difference, you no longer use case when, using case in because
in is going to be the keyword that's going to be mainly used for pattern matching even out of cases.
So in this case, if the response that I get has a status success, I'm going to take whatever is in
the body and put it there and otherwise if it's an error, I'm going to fail and put it over there.
So it's, again, it kind of does the same. You could do the whole counterpoint to this presentation
is I could do it with an if, else if. You always can, but I do think this is a bit more verbose and
makes it more clear what you're trying to do because you can see the entire pattern. Whereas if you
wanted to do an if, you would have to open response and do if the status is success, then I want to
look at the body. For this example looks the same, but if you're dealing with big jasons from APIs
where everything is nested like four times and you have response body value and then you take the
first element and then the address and then whatever this starts to become more interesting.
Another thing that we get with pattern matching that we can do with case when is we get access to
guard closes. So whatever that allows us to do is I want response to match with this only if I'm not
in maintenance. So this gives us a bit more control over whether or not we want the pattern to match
because sometimes you might want to put patterns that are very similar, but you want to condition
them to something different. Another example and another thing that we can do with pattern matching,
so let's look at a more complex pattern. We have access to a lot of new tools. So for example, here,
what this thing here means is that I want to match this pattern where the ID is whatever I put on
top. If I didn't put it, then it would act as the one we store before and store it into the
variable ID, but by doing this I can tell it no, no, no, use the value that's already there and match
one that has 69 as an ID. I don't want anything else. And we also have access to splat operators,
kind of. So simple splat for arrays, double splat for hashes, the same as with method arguments.
So what this allows me to do is I want to take user and if the user is in an array with some
elements at the beginning, some elements at the end, and then somewhere in the middle,
an element with ID 69, I want to store the value of admin. So this is kind of equivalent to take
my entire array and do a detect where ID 69 and then print admin. So this kind of does the same
thing, but in a more flexible way because I can then kind of keep putting more patterns underneath
to filter out more stuff or try to find more elements. So how does it work? I kind of, at this
point in the talk, I kind of wanted to go through the same journey with pattern matching as I did
with a simple case. So try to open it up and look at the VM instructions and see how it works and
try and figure out what's underneath. The problem is that pattern matching is kind of new. So in the
Ruby VM, that is a lot of instructions to go through. So I ain't going to go through everything.
But there are a few things that we can see here. So for example here, we have the same response.
So that's the beginning of our case. So this calls the thing that's going to go in the case that we're
going to try and pattern match against the same. We're looking at pattern matching. So of course,
the thing called check match, we kind of kind of assume that it's going to match or pattern
against something. So the way, at least the way I understand it is that all of this is going to
build or pattern and then it's going to match it to continue. And if we look at the way it builds
the pattern, we can find one method that is interesting, which is this one, which is deconstruct
keys. And after looking at it a bit more and going to read out the documentation, this is what
Ruby used to do, at least for now, to do pattern matching. So you have two methods. One is called
deconstruct keys that is used on patterns that are hashes. And another one is called deconstruct,
which is used on pattern that are arrays. That make sense? And so this does all of the deconstruction
and then if the pattern that you're sending doesn't respond to the deconstruct keys or the
deconstruct method, then it's just going to give up and tell you to implement it yourself
so that it works. And after that, it's more of the same thing, right? So that's the second
pattern that we have. It's still trying to deconstruct them. And then eventually,
if it doesn't find anything, it's going to return a no match error.
So the interesting thing then is how do we implement it ourselves? So if you have your
class and you want it to be, you want to use pattern matching on it, then one thing that you can do
is use, is implement the deconstruct keys method. So in this case, we have a location and we want
to have a latitude and a longitude in the deconstruct keys. And then that allows us every time we
have a location to use pattern matching on it, because it's going to deconstruct this,
deconstruct this, and then see what matches. And so in this case, and interesting thing also is
inside of our pattern, we have access to everything that we've been talking about earlier. So in your
pattern, you can put classes, you can put reg X, you can put ranges in this case. And the only thing
I think we haven't seen before is this little thing magic that just takes like, it wants to match it
against this and then store it into the variable that we can then use for anything else.
And I think that's it. I've tried to go through everything.
I sped through that one, sorry.
We have so much time.
I didn't.
You used a variable that was not declared before.
Yeah, probably. Where? In the right address before?
One?
latitude. Did you declare length to be equal to new before?
No, you don't have to declare it before. Basically what this does is it takes this and then store it.
It takes whatever matches here. So that would be technically this and then store it into the
latitude variables. You don't have to declare it before. And what's the scope of that variable?
It's going to be a scope to whatever the case is in. Right? So if your case is defined in a method,
then you have access to it in the entire method.
This is in current? Yeah.
I think this might be, this might have been implemented in Ruby three.
And the first occurrence of pattern matching, the one with the case in, was experimental in 2.7
and then actually arrived in Ruby three. And they've been trying to push it a bit more in
subsequent versions. So now, for example, you don't necessarily need to have case. If you want it,
if you want to use pattern matching, then you can just write your variable in something and
use it as a predicate to see if it matches or not.
In your example where you're looking for an admin user in an array of users and you have those
operations at the back, does that work if your admin user is the first or the last?
Yeah, yeah, yeah, yeah. Then the, then the,
Like it might not. Yeah, fair. Yeah, yeah, definitely fair.
What this will do is it will put nil in here and nil in the other variable.
Right? It's like there's nothing after, there's nothing before or there's nothing before.
Yeah, that's the thing that I was a bit, if you about this,
basically the, shit, I have to go through all the animations. Sorry, bear with me.
It's going to scroll again on. Okay, sure, whatever, shit.
The argument that it takes is in case you only want to deconstruct some keys.
Right? So if that is, if you have a big object and you only want to deconstruct latitude,
for example, you could work it this way. That's what it's supposed to do.
In the example, I didn't go through the trouble of implementing all of it,
because if I want to write code big, I can write too much code.
And yeah, that's why.
So it was deconstruct for arrays and deconstruct keys, though.
Yeah.
And you can define deconstruct as well. If you've got a class that implements an interval or something.
Probably, yeah, yeah, I think so.
Just to take how stable you think the syntax is. Do you think it's going to stay the same?
Huh.
Would you, would you, would you, would you want to do, for example, like the, the, the, the, the,
the, the, the, the, the, the, the, I think it's going to stay the same. No, sorry.
Yeah. I know I was thinking in my head, I think it's going to stay the same because it's the
exact same syntax that Alexia uses, for example. Like they've probably been inspired from other
languages and used it. And so I'm expecting it to stay the same. But then again, I don't know.
I think right now I'm still, I'm trying, I tried to push for it in very simple use cases.
So usually in a, if we get, if we have to make an API call, that's probably the best,
like, foot in the door to get it working in your code basis. Like, because that's the thing that
seems the most obvious, right? I get an answer and then I can, not only fetch the status,
but assign everything in the answer and then give it to another method. I think that's a bit,
not a frontend dev. So don't quote me on this at all. But it looks a bit like the object
deconstruction thing from JavaScript. Or you can get an object and then assign all the variables
into it. In this use case, I think it's a good first step to implement it in a code base. I
wouldn't go all out and start putting deconstruct keys in every class.
That would be really, I really hope that, I really hope they put it in Ruby at some point. I don't
think that's in the plans right now. I think the idea, the main idea behind was, like, when they
put it in Ruby at all, pattern matching in 2.7, it was kind of touch and go. People were discussing
a lot about do we want this in our code base because pattern matching in the collective
brain is usually more functional than object oriented. But now that it's there and it's past
the experimental and it's now stable, I think they're eventually going to do it. It'd be a shame not to, right?
Do you think some of this stuff is going to end up in the Ruby style guy?
And be something like Ruby cop goes and says, no, no, you don't want to do that.
You don't want to do that.
You want to use this instead.
Probably not in the near future. Because I think people are still very much like trying to figure
out what the good style is. Even when I was preparing this, I couldn't find a lot of examples.
So I kind of came up with what I think would look the best. But I don't think for now, at least,
there are a lot of established guidelines.
We good? Cool.
Nice.