DEV Community

Jeremy Woertink
Jeremy Woertink

Posted on • Edited on

My favorite things in Crystal Lang

When learning a new language, there will be some moments that really make you appreciate that language. And occasionally, some moments that make you question your sanity.

As I have been learning Crystal, there's a few things that I find myself saying "I love this!" giving me those same feelings I had for Ruby 12+ years ago. This list is the "out-of-the-box" features, methods, and constructs in Crystal Lang that I had to tell others about after I learned it.

NOTE: before some of you say it, I know that these are not original language ideas

Method overloading

This is a concept where a class can have multiple methods with the same name. The way the compiler decides which method to call is based on the arguments that are passed (or not passed).

The thing I love about this is a lot of times you want a method to be called something specific, but you have to use it in a few different ways.

class Pagination

  def initialize(opts = {} of String => String)
    initialize(opts["page"].to_i, opts["per"].to_i
  end

  def initialize(page : Int32, per : Int32)
    # setup the class
  end
end

Pagination.new({"page" => "1", "per" => "12"})
Pagination.new(1, 12)
Enter fullscreen mode Exit fullscreen mode

In this example, you can have multiple ways to instantiate an object, but best of all, they can all funnel down to the actual implementation. This just gives you the ability to make a friendly DSL for the developers, while still keeping your implementation details how you need them.

Other applications for this might be splitting your methods out for one to handle successful use, and another to handle error handling, or even several for different types of errors!

def call(some_string : String)
  # do thing since a string was passed
end

def call(not_a_string : Nil)
  raise "some error instead because nil was passed"
end
Enter fullscreen mode Exit fullscreen mode

Instance Variables

Speaking of initialize... This is awesome.

# Setting instance variable values in crystal
class Spot
  def initialize(@x : Int32, @y : Int32)
  end
end

# Setting instance variable values in ruby
class Spot
  def initialize(x, y)
    @x = x
    @y = y
  end
end
Enter fullscreen mode Exit fullscreen mode

When you have just 2 arguments, it's not a huge deal. You generally want to keep the amount of arguments down anyway; however, have you looked at some art/game type setups? looking at you gosu. Having these setup when you define the method just makes things less noisy in your code.

Tuples

These are collections of objects with a known size, and types in them. Since they don't exist in Ruby, PHP, or JavaScript, I never really knew what they were for.

class Thing
  @x : Tuple(String, String)
end
Enter fullscreen mode Exit fullscreen mode

In this example, I know that the instance variable @x will always and only ever contains 2 strings like {"this", "that"}. That's it. I defined it to be strictly 2 strings, and you can't add or remove from it. You could use an Array and put 2 elements in it, but from what I understand (which could be wrong) is that the Tuple is going to have a performance benefit since the compiler does a lot less guess work. You still use the Tuple like you would an array @x[0] == "this" @x[1] == "that" @x.each ....

Map shortcut

You can do some fancy shorthand stuff in Ruby with blocks like things.map(&:to_s) which would map the to_s method on to each of the things. But if things was an array of hashes, and you wanted to pull out a value of a specific key, you couldn't use the shortcut. In crystal, you can!

data = [{a: 1, b: 2}, {a: 3, b: 4}]
data.map(&.[:a]) # in crystal

data.map {|h| h[:a]} # in ruby
Enter fullscreen mode Exit fullscreen mode

Macros

A macro is some code that gets ran before your app is compiled. The macro code is expanded in to other code. If you've ever built a web application using some templating language (like ERB in ruby), then it's sort of like that. They're still kind of confusing to me, but here's a small example.

macro run_tests
  {% for number in ["one", "two", "three"] %}
    def test_{{number.id}}
      puts "Testing {{number.id}}"
    end
  {% end %}
end
# actually run the macro
run_tests

# This would expand to:

def test_one
  puts "Testing one"
end
def test_two
  puts "Testing two"
end
def test_three
  puts "Testing three"
end
Enter fullscreen mode Exit fullscreen mode

This macro would expand out to the three methods being defined, but only where you would actually call the run_tests method. It's sort of a way to get some metaprogramming in to your code to make things a bit more simple. The nice thing though is this expands before (during) the compilation of your program. Which means that when you run the program, those methods exist, and have already been type checked, etc...

Speaking of macros... There's a tool you can use from your command line with crystal to expand these macros out and see what they really look like.

macro test
  [
  {% for x in [0,1,2] %}
    {% for y in [4,5,6] %}
    [{{x.id}}, {{y.id}}],
    {% end %}
  {% end %}
  ]
end

test
Enter fullscreen mode Exit fullscreen mode
$ crystal tool expand -c test.cr:11:1 ./test.cr
1 expansion found
expansion 1:
   test

# expand macro 'test' (/Users/jeremywoertink/Development/crystal/test.cr:1:1)
~> [[0, 4], [0, 5], [0, 6], [1, 4], [1, 5], [1, 6], [2, 4], [2, 5], [2, 6]]
Enter fullscreen mode Exit fullscreen mode

T

The constant T is convention for this nifty feature, though, I'm sure you could really use any constant name (that doesn't conflict). Let's say you have a module that does some cool stuff, but this module requires some special types. You can setup the module to take this anonymous type T and have it as your placeholder.

module Shared::PaginationComponent(T)
  private def render_pagination(pagination : T)
    # do pagination rendering
  end
end
Enter fullscreen mode Exit fullscreen mode

Ok, so this module takes the T because the method takes an argument of pagination that's type T. In my case, this T could actually be a collection of User objects, or Video objects, or Site objects, etc... I could do something like

def render_pagination(pagination : User::Collection | Video::Collection | Site::Collection | Whatever::Collection)
end
Enter fullscreen mode Exit fullscreen mode

but then I would have to eventually figure out which of those it is, and then type cast .as(User::Collection). Instead, with this I do

class User::IndexPage
  include Shared::PaginationComponent(User::Collection) # see! It know exactly which to use here
end

class Site::IndexPage
  include Shared::PaginationComponent(Site::Collection)
end
Enter fullscreen mode Exit fullscreen mode

Make

Ok, so Make actually has nothing to do with Crystal. I'm adding this in though because I've never had a reason to learn Make. I've seen some gnarly Makefiles in the past, and they've always scared me. Since starting with crystal, I've learned to use Make, and write my own Makefile to build my app. It makes me feel like a "real" programmer.

OUT_DIR=bin
build: clean
    @mkdir -p $(OUT_DIR)
        @crystal build --release -o $(OUT_DIR)/app src/my_app.cr
clean:
    rm $(OUT_DIR)/app
Enter fullscreen mode Exit fullscreen mode

This is a simple Makefile that you would put in the root of your project, then run make build to build your crystal project and put it in to your bin directory.

More

There's tons more I could talk about, but I think this is long enough. I'll probably add more specifics in later blogs as I start to understand some stuff better.

Top comments (3)

Collapse
 
shayneoneill profile image
shayneoneill

Be careful with those macros. One thing I've learned from C++ is they are amazing tools for getting stuff done, but they CAN get in the way of comprehensibility and debugging, as essentially a form of magic. I've had a few times debugging both in C++ and Crystal where I've looked at the stack dump or crystals compile error dump (Which by the way is amazing, Crystal really does go that extra distance to try and explain where you've gone wrong and give suggestions on fixing it) and thought "Wait... this isnt what I wrote, whats happening here???"

Like, take C++ boost library. Its an amazing piece of software architecture. But I've lost count the amount of times I've wanted to throw my keyboard out the window (granted, anger attacks are part of life for C++ coding) because something deep in a macro has upset the gods, and the damn thing wont explain to me why.

Collapse
 
shayneoneill profile image
shayneoneill • Edited

For me the Jewel in Crystals crown is the type system. I've recently found myself working with some fairly large Swift codebases, and gotten a good chance to compare and contrast Swifts solution to Nulls and Crystals. Swift goes with boxing/unboxing , Crystal goes with Union typing. To me its much easier to reason about Crystals type system than having to constantly wrestle with boxing/unboxing semantics in Swift, and then even after that , you still dont get the nearly-but-not-quite dynamism of Crystals union types.

I genuinely feel every time Crystal trips me over and forces me to rethink something that could potentially suffer NULL harm, its making me into a better, more defensive, programmer.

I do wish Crystal had typed Nulls. Like there IS an issue where sometimes you want nulls, say as a field coming out of a database, but you'd like to distinguish between "Im a dummy and created null harm by not initializing properly" and "This field returned a nill", because the solutions to both are very different.

Collapse
 
jwoertink profile image
Jeremy Woertink

Hahah. Yup! That does look good