Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Crystal for Rubyists

Crystal for Rubyists

We at JetRockets use Crystal in production since September, 2019.

In these slides I show how Crystal differs from Ruby, how Ruby developers can start using Crystal fast and why we decided to include it in our development stack.

Igor Alexandrov

February 29, 2020
Tweet

More Decks by Igor Alexandrov

Other Decks in Programming

Transcript

  1. – favourite client “We need an ability to upload data

    about all real estate loans in US”
  2. • ruby is slow (to be honest – not); •

    we will hit memory leaks again; • why not try something new?
  3. Crystal Language • Syntax inspired by Ruby • Compiled (based

    on LLVM) • With type inference and union types • With concurrency and parallelism • Does not support Windows
  4. Type Inference (Ruby) if some_condition a = 1 a.abs #

    will never fail else a = "hello" a.upcase # will never fail too end a.upcase # can fail at runtime
  5. Type Inference (Ruby) if some_condition a = 1 a.abs #

    will never fail else a = "hello" a.upcase # will never fail too end a.upcase # can fail at runtime
  6. Type Inference (Crystal) if some_condition a = 1 # a

    is Int32 a.abs else a = "hello" # a is String a.upcase end a.upcase # a is Int32 or String
  7. Type Inference (Crystal) if some_condition a = 1 # a

    is Int32 a.abs else a = "hello" # a is String a.upcase end a.upcase # a is Int32 or String
  8. Type Composition struct Int32OrString { int type_id; union { int

    int_value; char[] string_value; } data; }
  9. Type Composition if something_that_you_know_will_happen a = 1 end # You

    can guarantee that a is not a nil a.not_nil!.abs
  10. Type Composition class Object def not_nil! self end end class

    Nil def not_nil! raise NilAssertionError.new end end a = some_condition ? 1 : nil a.not_nil!.abs
  11. Type Composition a = 1 while some_condition a # here

    a is actually Int32 or String a = false # here a is Bool a = "hello" # here a is String a.size # ok, a is String end a # here a is Int32 or String
  12. Type Filter a = some_condition ? 1 : nil #

    a is Int32 or Nil if a a.abs # a is Int32 end # compiler assumes the variable is not nil # inside the if branch
  13. Types in Crystal • Raise on compile-time if types mismatch

    • Type Inference • Type Composition • Type Filter
  14. Concurrency Sandwich example • you’re chopping an onion, putting it

    to fry.; • while it’s being fried, you’re chopping a tomato; • you’re not doing all of those things at the same time;
  15. Parallelism Sandwich example • you have at least four arms

    (2 pairs); • you’re chopping an onion, putting it to fry; • while you’re cutting an onion, you’re also chopping a tomato; • you are doing at least two things at the same time;
  16. Metaprogramming • no code modifications at runtime • no #send,

    #method_missing, #instance_eval… • Crystal has macros
  17. Simple Example (Ruby) class Point def initialize(x, y) @x, @y

    = x, y end def x @x end def y @y end end
  18. Simple Example (Ruby) def getter(*names) names.each do |name| class_eval "def

    #{name}; @#{name}; end" end end class Point def initialize(x, y) @x, @y = x, y end getter :x, :y end
  19. Simple Example (Crystal) class Point def initialize(@x : Int32, @y

    : Int32) end def x @x end def y @y end end
  20. Simple Example (Crystal) macro attr_reader def x @x end end

    class Point def initialize(@x : Int32, @y : Int32) end attr_reader def y @y end end
  21. Simple Example (Crystal) macro attr_reader(*names) {% for name in names

    %} def {{name}} @{{name}} end {% end %} end class Point def initialize(@x : Int32, @y : Int32) end attr_reader x, y end
  22. Simple Example (Crystal) macro attr_reader(*names) {% for name in names

    %} def {{name}} @{{name}} end {% end %} end class Point def initialize(@x : Int32, @y : Int32) end attr_reader x, y # this is not an error end
  23. Simple Example (Crystal) macro attr_reader(*names) {% for name in names

    %} def {{name}} @{{name}} end {% end %} end class Point def initialize(@x : Int32, @y : Int32) end attr_reader :x, :y end
  24. Simple Example (Crystal) class Point def initialize(@x : Int32, @y

    : Int32) end def :x @:x end def :y @:y end end
  25. Simple Example (Crystal) macro attr_reader(*names) {% for name in names

    %} def {{name.id}} @{{name.id}} end {% end %} end class Point def initialize(@x : Int32, @y : Int32) end attr_reader :x, :y end
  26. How Macros Works? • AST nodes are passed as arguments

    • These are processed to produce a string • The string is parsed to a valid expression • The expression is “pasted” into the program
  27. Shrine Example (Ruby) def plugin(plugin, *args, **kwargs, &block) plugin =

    Plugins.load_plugin(plugin) if plugin.is_a?(Symbol) Plugins.load_dependencies(plugin, self, *args, **kwargs, &block) self.include(plugin::InstanceMethods) if defined?(plugin::InstanceMethods) self.extend(plugin::ClassMethods) if defined?(plugin::ClassMethods) self::UploadedFile.include(plugin::FileMethods) if defined?(plugin::FileMethods) self::UploadedFile.extend(plugin::FileClassMethods) if defined?(plugin::FileClassMethods) self::Attachment.include(plugin::AttachmentMethods) if defined?(plugin::AttachmentMethods) self::Attachment.extend(plugin::AttachmentClassMethods) if defined?(plugin::AttachmentClassMethods) self::Attacher.include(plugin::AttacherMethods) if defined?(plugin::AttacherMethods) self::Attacher.extend(plugin::AttacherClassMethods) if defined?(plugin::AttacherClassMethods) Plugins.configure(plugin, self, *args, **kwargs, &block) plugin end
  28. – From documentation “Crystal does not support #include and #extend

    for adding modules to other modules or to create new class or instance methods at runtime.”
  29. Shrine Example (Crystal) macro load_plugin(plugin, **options) {% plugin = plugin.resolve

    %} {% PLUGINS << {decl: plugin, options: options} %} {% if plugin.constant(:InstanceMethods) %} include {{plugin.constant(:InstanceMethods)}} {% end %} {% if plugin.constant(:ClassMethods) %} extend {{plugin.constant(:ClassMethods)}} {% end %} end
  30. Shrine Example (Crystal) macro load_plugin(plugin, **options) # ... {% if

    plugin.constant(:FileClassMethods) %} class UploadedFile < Shrine::UploadedFile extend {{plugin.constant(:FileClassMethods)}} end {% end %} {% if plugin.constant(:FileMethods) %} class UploadedFile < Shrine::UploadedFile include {{plugin.constant(:FileMethods)}} end {% end %} end
  31. Shrine Example (Crystal) class Uploader < Shrine load_plugin( Shrine::Plugins::DetermineMimeType, analyzer:

    Shrine::Plugins::DetermineMimeType::Tools::File ) finalize_plugins! end
  32. Another Shrine Example (Ruby) class Shrine module ClassMethods def inherited(subclass)

    # ... file_class = Class.new(self::Attacher) file_class.shrine_class = subclass subclass.const_set(:Attacher, file_class) # ... end end end
  33. Another Shrine Example (Ruby) class Shrine module ClassMethods def inherited(subclass)

    # ... file_class = Class.new(self::Attacher) file_class.shrine_class = subclass subclass.const_set(:Attacher, file_class) # ... end end end
  34. Another Shrine Example (Crystal) class Shrine macro inherited # Reopen

    the class... class Attacher < Shrine::Attacher def self.shrine_class {{ @type }} end end end end
  35. Another Shrine Example (Crystal) class Uploader < Shrine plugin :determine_mime_type,

    analyzer: :file end puts Uploader::Attacher.shrine_class # => Uploader
  36. Macros Benefits • Macros gets evaluated at compile time (not

    a runtime) • Less chances to get a runtime error • No performance downgrades • Compiler can even perform an optimizations
  37. Macros Сons • Extremely hard to debug • Extremely hard

    to debug! • Only YOU can understand your macros • If you can do it without macros – do it :)
  38. Nice Macro Trick a = 1 pp! a # =>

    "a # => 1" pp! [1, 2, 3].map(&.to_s) # => "[1, 2, 3].map(&.to_s) # => ["1", "2", "3"]"
  39. Ruby Community • Everything is on GitHub • One spec

    library (yes, I’ve never used minitest) • One code formatting rules (I recently discovered Rufo) • One deployment tool (who ever used Mina?) • One web framework :(
  40. Crystal Community • Young • A lot of people came

    from Ruby • Even more “standardised”
  41. Tooling • web frameworks (Kemal, Amber, Lucky); • background processing

    (sidekiq, it does not eat memory); • specs (Spec, Spectator); • ORMs (Clear, Jennifer.cr);
  42. Tooling • a lot is still to be done; •

    ability to make your OpenSource commitment; • we maintain Shrine.cr; • https://github.com/jetrockets/shrine.cr • https://jetrockets.github.io/shrine.cr
  43. YARD??? # Converts the object into textual markup given a

    specific `format` # (defaults to `:html`) # # == Parameters: # format:: # A Symbol declaring the format to convert the object to. This # can be `:text` or `:html`. # # == Returns: # A string representing the object in a specified # format. # def to_format(format = :html) # format the object end
  44. Documentation (Crystal) • included into compiler; • $ crystal docs

    • knows everything about params; • knows all return types; • generates HTML (even with sitemaps); • #exceptions
  45. Code Style and Analysis • included into compiler; • $

    crystal tool format • Ameba – static code analysis • like Rubocop for Ruby • with adequate configuration
  46. Ameba # This configuration file was generated by # `ameba

    --gen-config`on 2020-02-23 13:02:10 UTC using # Ameba version 0.11.0. # The point is for the user to remove these configuration # records one by one as the reported problems are removed # from the code base. # Problems found: 1 # Run `ameba --only Lint/ShadowingOuterLocalVar` for details Lint/ShadowingOuterLocalVar: Description: Disallows the usage of the same name as outer local variables for block or proc arguments. Enabled: true Severity: Warning Excluded: - spec/lib/uploaded_file_spec.cr
  47. Convert Ruby code to Crystal? • https://github.com/DocSpring/ruby_crystal_codemod; • ruby gem;

    • fork of Rufo (an alternative code formatter for Ruby); • produces semi-valid Crystal code; • https://github.com/jetrockets/content_disposition.cr; • 70 lines of code :)