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

Ruby 3.2 features you probably haven't used yet

Ruby 3.2 features you probably haven't used yet

Avatar for Szymon Fiedler

Szymon Fiedler

November 23, 2023

More Decks by Szymon Fiedler

Other Decks in Programming

Transcript

  1. Anonymous arguments passing improvements def keywords(**) # accept keyword arguments

    foo(**) # pass them to the next method end def positional(*) # accept positional arguments bar(*) # pass to the next method end def positional_keywords(*, **) # same as ... foobar(*, **) end
  2. Endless methods def square(x) = x**2 square(100) # => 10000

    def dbg_args(a, b=1, c:, d: 6, &block) = puts("Args passed: #{[a, b, c, d, block.call]}") dbg_args(0, c: 5) { 7 } # Prints: Args passed: [0, 1, 5, 6, 7] def nuffin = () # def nuffin; end
  3. Regexp improvements Regexp.timeout = 2.0 # Global configuration set to

    two seconds /^x*y?x*()\1$/ =~ "x" * 45000 + "a" #=> Regexp::TimeoutError is raised in two seconds my_long_rexp = Regexp.new('^x*y?x*()\1$', timeout: 4) my_long_rexp =~ "x" * 45000 + "a" # Regexp::TimeoutError is raised in four seconds
  4. Pattern matching improvements person = {name: "John", children: [{name: "Mark",

    age: 12}, {name: "Butler", age: 9}], siblings: [{name: "Mary", age: 31}, {name: "Conrad", age: 38}] } case person in {name: "John", children: [{name: "Mark", age: age}]} p age end # those won't match because of :siblings key present
  5. Find pattern to the rescue person = {name: "John", children:

    [{name: "Mark", age: 12}, {name: "Butler", age: 9}], siblings: [{name: "Mary", age: 31}, {name: "Conrad", age: 38}] } case person in {name: "John", children: [*, {name: "Mark", age: age}, *]} p age end
  6. deconstruct_keys in Time # `deconstruct_keys(nil)` provides all available keys: timestamp

    = Time.now.deconstruct_keys(nil) #=> { :year=>2023, :month=>11, :day=>23, :yday=>327, :wday=>4, :hour=>12, :min=>59, :sec=>30, :subsec=>(450371/1000000), :dst=>false, :zone=>"CET" } # Usage in pattern-matching: case timestamp in year: ...2022 puts "Last year!" in year: 2022, month: 1..3 puts "Last year's first quarter" in year: 2023, month:, day: puts "#{day} of #{month}th month!" end
  7. deconstruct_keys in Date and DateTime require 'date' Date.today.deconstruct_keys(nil) #=> {:year=>2023,

    :month=>1, :day=>15, :yday=>15, :wday=>0} DateTime.now.deconstruct_keys(nil) # => {:year=>2023, :month=>1, :day=>15, :yday=>15, :wday=>0, :hour=>17, :min=>19, :sec=>15, :sec_fraction=>(478525469/500000000), :zone=>"+02:00"}
  8. Matching with class check case distance in Measure(amount:, unit: 'km')

    puts "It is #{amount} kilometers away" # ... end
  9. Pattern matching with MatchData case db_connection_string.match(%r{postgres://(\w+): (\w+)@(.+)}) in 'admin', password,

    server # do connection with admin rights in 'devuser', _, 'dev-server.us-east- 1.rds.amazonaws.com' # connect to dev server in user, password, server # do regular connection end
  10. YJIT Yet Another Just In Time compiler it provides significantly

    higher performance, but also uses more memory – official part of the Ruby 3.2 runtime. – default for Ruby on Rails on Ruby 3.3 –
  11. Data class Measure = Data.define(:amount, :unit) # Positional arguments constructor

    is provided distance = Measure.new(100, 'km') #=> #<data Measure amount=100, unit="km"> # Keyword arguments constructor is provided weight = Measure.new(amount: 50, unit: 'kg') #=> #<data Measure amount=50, unit="kg"> # Alternative form to construct an object: speed = Measure[10, 'mPh'] #=> #<data Measure amount=10, unit="mPh"> # Works with keyword arguments, too: area = Measure[amount: 1.5, unit: 'm^2'] #=> #<data Measure amount=1.5, unit="m^2"> # Argument accessors are provided: distance.amount #=> 100 distance.unit #=> "km"
  12. All arguments are mandatory Measure = Data.define(:amount, :unit) Measure.new(amount: 1)

    # in `initialize': missing keyword: :unit (ArgumentError) Measure.new(1) # in `initialize': missing keyword: :unit (ArgumentError)
  13. Optional block for define Measure = Data.define(:amount, :unit) do def

    <=>(other) return unless other.is_a?(self.class) && other.unit == unit amount <=> other.amount end include Comparable end Measure[3, 'm'] < Measure[5, 'm'] #=> true Measure[3, 'm'] < Measure[5, 'kg'] # comparison of Measure with Measure failed (ArgumentError)
  14. Immutable, but Event = Data.define(:time, :weekdays) event = Event.new('18:00', %w[Tue

    Wed Fri]) #=> #<data Event time="18:00", weekdays= ["Tue", "Wed", "Fri"]> # There is no #time= or #weekdays= accessors, but changes are still possible: event.weekdays << 'Sat' event #=> #<data Event time="18:00", weekdays= ["Tue", "Wed", "Fri", "Sat"]>
  15. Good for homogenous data classes class HTTPFetcher Response = Data.define(:body)

    NotFound = Data.define # ... implementation end #<data HTTPFetcher::Response body="<html..."> #<data HTTPFetcher::NotFound> case fetcher.get(url) in HTTPFetcher::Response(body) # process body variable in HTTPFetcher::NotFound # handle not found case end
  16. Great for value objects Measure = Data.define(:amount, :unit) Measure[1, 'km']

    == Measure[1, 'km'] #=> true Measure[1, 'km'] == Measure[2, 'km'] #=> false Measure[1, 'km'] == Measure[1, 'm'] #=> false Measurement = Data.define(:amount, :unit) # Even though Measurement and Measure have the same "shape" # their instances are never equal Measure[1, 'km'] == Measurement[1, 'km'] #=> false
  17. Less boilerplate for the same effect and even more features

    Measure = Data.define(:amount, :unit) class Measure def intialize(amount:, unit:) @amount = amount @unit = unit end attr_reader :amount, :unit def eql?(other) other.instance_of?(Measure) && amount.eql?(other.amount) && unit.eql?(other.unit) end alias == eql? def hash Measure.hash ^ [amount, unit].hash end end
  18. Differentiate nil from none Measure = Data.define(:amount, :unit) do NONE

    = Data.define def initialize(amount:, unit: NONE.new) super(amount: Float(amount), unit:) end end Measure.new('10', 'km') # => #<data Measure amount=10.0, unit="km"> Measure.new(10_000) # => #<data Measure amount=10000.0, unit=#<data NONE>> case measure in Measure(amount:, unit: NONE) what_a_nice_number_without_context(amount) in Measure(amount:, unit: 'km') divide_by_1000(amount) in Measure(amount:, unit: 'ft') make_it_metric_ffs(amount) end
  19. with Point = Data.define(:x, :y) origin = Point.new(x: 0, y:

    0) up = origin.with(x: 1) right = origin.with(y: 1) up_and_right = up.with(y: 1) p origin # #<data Point x=0, y=0> p up # #<data Point x=1, y=0> p right # #<data Point x=0, y=1> p up_and_right # #<data Point x=1, y=1> out = origin.with(z: 1) # ArgumentError: unknown keyword: :z some_point = origin.with(1, 2) # ArgumentError: expected keyword arguments, got positional arguments