Kirk Haines
Principal Developer Relations Engineer w/ New Relic
@wyhaines everywhere
https://www.therelicans.com/wyhaines
This term is heavily overloaded!
Dynamic
https://en.wikipedia.org/wiki/Dynamic_programming
I do not mean this:
...it refers to simplifying a complicated problem by breaking it down into simpler sub-problems in a recursive manner.
Dynamic
https://en.wikipedia.org/wiki/Dynamic_programming_language
We kind of mean this:
...a class of high-level programming languages, which at runtime execute many common programming behaviours that static programming languages perform during compilation...object...alteration...reflection...macros...
Dynamic
Crystal is a compiled, statically typed language, and it is not a dynamic language.
Dynamic
All Done! Time for the next talk!
Metaprogramming
https://en.wikipedia.org/wiki/Metaprogramming
Metaprogramming is a technique where code is treated as data. Also:
Metaprogramming can be used to move computations from run-time to compile-time, to generate code using compile time computations, and to enable self-modifying code.
Metaprogramming
Metaprogramming tools overlap Dynamic Language tools,
and they enable those tools in Crystal.
Metaprogramming
Languages like Javascript and Ruby support eval()
Metaprogramming
Crystal doesn't support eval()*
(interpreted Crystal, unveiled by Asterite earlier, might, on some levels, change this)
Metaprogramming
Crystal does have:
Metaprogramming
Crystal does have:
Metaprogramming
Crystal does have:
Metaprogramming
Crystal does have:
https://chrisseaton.com/phd/specialising-ruby.pdf
Metaprogramming can also be described as a dynamic language feature.
What if you want more information?
Is it really only 40µs to handle a request?
What is the performance breakdown of different phases?
Can I get this without rewriting my app or Kemal?
Interesting....
How does that work, anyway?
Classes in Crystal can be reopened
#previous_def is the magic
#previous_def calls the previous definition of the method
This permits writing code which injects itself into other classes/structs, wrapping and overriding or proxying those methods.
In Ruby, #send is used to call methods based on information that can not be known at compile time.
#send is commonly used in Ruby metaprogramming.
It can call methods based on their names, which might come from command line inputs or configuration file inputs or other sources not known until runtime.
Problem:
Software accepts complex configuration from potentially nested, serialize YAML files. You want to provide a CLI option that can override anyconfig value from the command line. You DO NOT want to attempt to explicitly handle all possible config options, though. You want a generic solution that will just call the right methods based on CLI options.
command -k foo:bar -k deeper:nested:key:value
Requirements:
Requirements:
Requirements:
# With Procs
method = ->(obj : Foo) { obj.a },
method2 = ->(obj : Foo, val : Int32) { obj.b(val) }
method.call(receiver)
method2.call(receiver, 123)
With Procs:
Requirements:
# With records (Structs)
record Call_a, obj : Foo do
def call
obj.a
end
end
record Call_b, obj : Foo, val : Int32 do
def call
obj.b(val)
end
end
Call_a.new(receiver).call
Call_b.new(receiver, 123).call
With Records:
Requirements:
Requirements:
SendLookup = {
"a" => Call_a
}
SendLookupInt32 = {
"b" => Call_b
}
Requirements:
Requirements:
def send(method)
SendLookup[method].call(self)
end
def send(method, arg1 : Int32)
SendLookupInt32[method].call(self, arg1)
end
Consider the following class:
class Foo
def a(val : Int32)
val + 7
end
def b(x : Int32, y : Int32)
x * y
end
def c(val : Int32, val2 : Int32)
val * val2
end
def d(xx : String, yy : Int32) : UInt128
xx.to_i.to_u128 ** yy
end
end
Send capability could be built like this:
module FooSends
SendLookupInt32 = {
"a": ->(obj : Foo, val : Int32) { obj.a(val) },
}
SendLookupInt32Int32 = {
"b": ->(obj : Foo, x : Int32, y : Int32) { obj.b(x, y) },
"c": ->(obj : Foo, val : Int32, val2 : Int32) { obj.c(val, val2) },
}
SendLookupStringInt32 = {
"d": ->(obj : Foo, xx : String, yy : Int32) { obj.d(x, y) }
}
def send(method, arg1 : Int32)
SendLookupInt32[method].call(self, arg1)
end
def send(method, arg1 : Int32, arg2 : Int32)
SendLookupInt32Int32[method].call(self, arg1, arg2)
end
def send(method, arg1 : String, arg2 : Int32)
SendLookupStringInt32[method].call(self, arg1, arg2)
end
end
For a big class, or if there are many classes, this would be no fun at all.
Requirements:
Requirements:
...
4. Have Crystal build the code itself instead of requiring that it be hand-built.
Macros!
Macros are code, essentially written in an interpreted subset of Crystal, which writes
new code that will be inserted into the codebase and compiled with it.
Macros!
Macros have access to reflection data -- types, methods, classes, annotations, etc...
Macros!
Macros can build everything that we need!
require "send"
class Foo
include Send
def a(val : Int32)
val + 7
end
def b(x : Int32, y : Int32)
x * y
end
def c(val : Int32, val2 : Int32)
val * val2
end
def d(xx : String, yy : Int32) : UInt128
xx.to_i.to_u128 ** yy
end
end
Send.activate
f = Foo.new
pp f.__send__("b", 23, 37)
The full implementation is a talk all by itself.
Take a look at: https://github.com/wyhaines/Send.cr
In Ruby, #method_missing is a method that is called at runtime when an attempt is made to invoke a method that doesn't exist in the current scope.
Crystal implements this as a compile-time feature using a macro.
Crystal's metaprogramming capabilities and the dynamic language qualities that can be built from them are powerful.
All of the code for this presentation, and the slide deck, will be pushed to GitHub@
https://github.com/wyhaines/crystal-conf-1.0-dynamic-crystal