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

Functional Ruby for Functional Programmers

keithrbennett
January 25, 2025
13

Functional Ruby for Functional Programmers

This presentation explores Ruby from a functional programming perspective. While Ruby is primarily an object-oriented programming language, it also supports functional programming (FP) through the following:

* lambda and non-lambda instances of the Proc class, which are closures, first class objects, and higher order functions
* methods (instance and class), which can be converted to objects that behave like first class functions
* a rich Enumerable library that supports lazy evaluation, allowing efficient handling of infinite lists or large datasets.
* partial application and currying
* pattern matching

In addition, the features and limitations of immutability, such as the `freeze` method and the `hamster` gem (library), will be discussed.

The standard Ruby implementation can interface with code written in C, and the JRuby implementation runs on the JVM and (like Clojure and Scala) can call Java code in the same process. If you should need a language that can be run on both platforms and support both FP and OOP, Ruby could be a great choice.

keithrbennett

January 25, 2025
Tweet

Transcript

  1. Who I Am Keith R. Bennett, @keithrbennett on many social

    media platforms From the US, living mostly in Southeast Asia Software engineer primarily Ruby extensive previous experience in C, C++, and Java currently studying AI Fan of functional programming, having briefly studied Clojure and Erlang Open to employment and consulting opportunities https://www.bbs-software.com Functional Ruby -- Keith Bennett -- Functional Conf 2025 2
  2. Ruby: OO with FP Inspiration Mainly an Object-Oriented (OO) language

    Created by Yukihiro Matsumoto (Matz), first released in 1995 Influences from Lisp, Perl, Smalltalk, Eiffel, and Ada Aims to be easy to read and write, and fun to work with Functional Ruby -- Keith Bennett -- Functional Conf 2025 3
  3. Major Ruby Implementations C-based implementation (CRuby) Reference implementation Most widely

    used implementation New features come here first Like Python, has a Global Interpeter Lock (GIL) so only 1 CPU is used Java-based implementation (JRuby) Access to wealth of Java libraries Use mature and stable JVM (Java Virtual Machine) All CPU's are available for threads Functional Ruby -- Keith Bennett -- Functional Conf 2025 4
  4. Lisp Influences on Ruby Closures: code blocks, procs, lambdas, not

    associated with objects Built-in Enumerable functionality including these and many more: Map (Collect) Select (Filter) Reduce (Fold, Inject) Symbols (Lisp: 'foo , Ruby: :foo ) Has REPL's ( irb , pry ) Language Array Doubling Example Ruby [1, 2, 3, 4].map { |x| x * 2 } # [2, 4, 6, 8] Lisp (mapcar (lambda (x) (* x 2)) '(1 2 3 4)) Clojure (map #(* % 2) [1 2 3 4]) # (2 4 6 8) Functional Ruby -- Keith Bennett -- Functional Conf 2025 5
  5. Code Blocks Ruby's code blocks are code literals passed to

    methods: 3.times { |i| puts "Iteration #{i + 1}" } produces: Iteration 1 Iteration 2 Iteration 3 The implementation of times in the Integer class that uses the code block is similar to this: class Integer def times return enum_for(:times) unless block_given? (0...self).each { |n| yield n } self end end Functional Ruby -- Keith Bennett -- Functional Conf 2025 6
  6. {} and do/end {} and do/end are interchangeable as delimiters,

    though by convention curly braces are used for a single line, and do/end for multiple lines. The following two expressions are interchangeable: 5.times { |i| puts "Iteration #{i + 1}" } 5.times do |i| puts "Iteration #{i + 1}" end Functional Ruby -- Keith Bennett -- Functional Conf 2025 7
  7. Functions and Methods as First Class Objects Instances of Proc

    (both lambda and nonlambda) Code blocks converted to Proc instances using & Class and instance methods converted to Method objects Functional Ruby -- Keith Bennett -- Functional Conf 2025 8
  8. Lambda and Nonlambda Procs Both procs and lambdas are instances

    of the same Proc class Can be created with the lambda and proc methods ( proc for nonlambda), and shorthand -> for lambda: la = lambda {}; la2 = -> {} pr = proc {}; pr2 = Proc.new {} Can query to see which kind of Proc it is: [lambda {}, proc {}].map(&:lambda?) # [true, false] Much unfortunate confusion in the terminology When speaking, proc and Proc sound the same. Functional Ruby -- Keith Bennett -- Functional Conf 2025 9
  9. Defining lambdas and procs with Arguments Code Block Style lambda

    { |x, y| x + y } proc { |x, y| x + y } Proc.new { |x, y| x + y } Method Style ->(x, y) { x + y } Ruby lambdas, procs, code blocks, and methods support but do not require an explicit return keyword the last evaluated value will be returned an explicit use of return can be used for an early return Functional Ruby -- Keith Bennett -- Functional Conf 2025 10
  10. lambdas vs. procs: Arity Checking Lambdas have strict arity checking

    but procs and code blocks do not: lambdas: f = ->(lat, long) { p [lat, long] } f.call(12.34) # output: wrong number of arguments (given 1, expected 2) (ArgumentError) procs: p = proc { |lat, long| p [lat, long] } p.call(12.34) # output: [12.34, nil] Functional Ruby -- Keith Bennett -- Functional Conf 2025 11
  11. lambdas vs. procs: return Behavior A lambda's return only returns

    from the lambda itself A proc's (or code block's) return returns from the enclosing method Examples: def lambda_example my_lambda = lambda { return "Lambda" } my_lambda.call "Method" end # return value: "Method" def proc_example1 my_proc = proc { return "Proc" } my_proc.call "Method" # This line will never be reached end # return value: "Proc" def proc_example2 my_proc = proc { "Proc" } my_proc.call "Method" end # return value: "Method" Functional Ruby -- Keith Bennett -- Functional Conf 2025 12
  12. Dot Paren Shorthand for Call Ruby provides a .() shorthand

    that can be used instead of the call method name: doubler = ->(x) { x * 2 } puts doubler.call(5) # Output: 10 puts doubler.(5) # Output: 10 This shorthand can be used for any object that has a call method, including class and instance methods. Functional Ruby -- Keith Bennett -- Functional Conf 2025 13
  13. Callables Ruby's duck typing allows any object with a call

    method to be used as a callable. This means that not only lambdas and procs, but also classes and instances with a call method can be used interchangeably. Example: class Foo def self.call = puts "I am a class method." def call = puts "I am an instance method." end f = -> { puts "I am a lambda." } p = proc { puts "I am a proc." } [Foo, Foo.new, f, p].each(&:call) I am a class method. I am an instance method. I am a lambda. I am a proc. Functional Ruby -- Keith Bennett -- Functional Conf 2025 14
  14. Immutability: The freeze Method Ruby objects are mutable by default.

    To make an object immutable, use the freeze method. Example: a = ['foo', 'bar'] # create mutable array ['foo', 'bar'] a << 'baz' # ['foo', 'bar', 'baz'] - mutate it p a.frozen? # Output: false a.freeze p a.frozen? # Output: true a << 'another string' # RuntimeError: can't modify frozen array (RuntimeError) # However, the array's *elements* may still be mutable: a[0] << 'd' # Array is now ["food", "bar", "baz"] ('food', not 'foo') a.each(&:freeze) # Freeze all elements; but again, this is a shallow freeze a.first << 'ie' # can't modify frozen String: "food" (FrozenError) Custom classes must define their own freeze method. Can they be trusted? Objects created by dependent libraries will probably not be immutable. Functional Ruby -- Keith Bennett -- Functional Conf 2025 15
  15. Immutability: The hamster Gem Operations that would typically modify the

    array (like adding or removing elements) will return a new instance instead of altering the original. For example: require 'hamster/vector' v = Hamster.vector(1, 2, 3) new_v = v.add(4) p v.equal?(new_v) # Output: false (`equal?` checks for object identity) Hamster supports method chaining: new_v = v.add(4).remove(1).map { |x| x * 2 } Functional Ruby -- Keith Bennett -- Functional Conf 2025 16
  16. Converting a Method to a First Class Object class MyClass

    def my_method(arg1, arg2) "The args were: #{arg1} and #{arg2}" end end instance = MyClass.new method_obj = instance.method(:my_method) result = method_obj.("one", "two") puts result # Output: The args were: hello and world Functional Ruby -- Keith Bennett -- Functional Conf 2025 17
  17. Rich Enumerable Library a = [1, 2, 3, 4, 5]

    # each a.each { |x| print x } # '12345' # map (synonym: collect) a.map { |x| x * x } # [1, 4, 9, 16, 25] # select (synonyms: filter, find_all) a.select { |x| x % 2 == 0 } # [2, 4]; a.select { |x| x.even? } # [2, 4] a.select(&:even?) # [2, 4] # reject a.reject(&:odd?) # [2, 4] # partition evens, odds = a.partition(&:even?) # [[2, 4], [1, 3, 5]] Functional Ruby -- Keith Bennett -- Functional Conf 2025 18
  18. Rich Enumerable Library (continued) a = [1, 2, 3, 4,

    5] # inject (synonyms: reduce, fold) a.inject(0) { |sum, x| sum + x } # 15 a.inject { |sum, x| sum + x } # 15 a.inject(&:+) # 15 a.sum # 15 # all/any/none a.all?(&:even?) # false a.any?(&:even?) # true a.none?(&:even?) # false # compact (removes nils) [nil, 'hi', nil].compact # ['hi'] # Set operations a & [3,6] # [3] a | [3,6] # [1, 2, 3, 4, 5, 6] a - [3,6] # [1, 2, 4, 5] a ^ [3,6] # [1, 2, 4, 5] [2,3,3,4] - [3] # [2, 4] # removes all elements of that value, not just first Functional Ruby -- Keith Bennett -- Functional Conf 2025 19
  19. Rich Enumerable Library (continued) a = [1, 2, 3, 4,

    5] # max, min, minmax a.max # 5 a.min # 1 min, max = a.minmax # [1, 5]; min = 1, max = 5 # cycle a.cycle.take(10) # [1, 2, 3, 4, 5, 1, 2, 3, 4, 5] # tally - provides counts of each unique value [:a, :a, :b, :c, :c, :c].tally # { a: 2, b: 1, c: 3} # shuffle - shuffles the array shuffled = a.shuffle # sample - returns a random element(s) from the array a.sample # 2 a.sample(2) # [2, 4] a.sample(10) # [2, 3, 4, 1, 5] Functional Ruby -- Keith Bennett -- Functional Conf 2025 20
  20. Ruby Enumerable and Collection Methods Return New Instance Most Ruby

    methods that work with enumerables and collections return new objects. a = [1, 2, 3] doubled = a.map { |n| 2 * n } # a: [1, 2, 3], doubled: [2, 4, 6] puts a.equal?(doubled) # false Methods that mutate the object usually have names ending with ! a = [1, 2, 3] doubled = a.map! { |n| 2 * n } # a: [2, 4, 6], doubled: [2, 4, 6] puts a.equal?(doubled) # true Functional Ruby -- Keith Bennett -- Functional Conf 2025 21
  21. Lazy Evaluation (1..Float::INFINITY).lazy.map { |x| x * x }.take(5) #

    [1, 4, 9, 16, 25] Functional Ruby -- Keith Bennett -- Functional Conf 2025 22
  22. Partial Application & Currying Partial Application greet = ->(greeting, name)

    { "#{greeting}, #{name}!" } hello = ->(name) { greet.("Hello", name) } puts hello.("Alice") # Output: Hello, Alice! Currying greet = ->(greeting, name) { "#{greeting}, #{name}!" } hello = greet.curry.("Hello") puts hello.("Alice") # Output: Hello, Alice! Functional Ruby -- Keith Bennett -- Functional Conf 2025 23
  23. Pattern Matching in Ruby Type Checking Guard Clauses Deconstruction (can

    define array and hash deconstruction in custom classes) def process_user_data(input) case input in [name, age, role] if age.to_i >= 18 # Guard to check age puts "Adult user: #{name} is #{age} years old and works as #{role}" in [first, *rest] if first == "admin" # Deconstruction with splat and guard puts "Admin account with additional data: #{rest.join(', ')}" in {name:, role:} # Hash deconstruction puts "User #{name} has role #{role}" else puts "Unmatched pattern" end end # Example usage: process_user_data(["Alice", "25", "developer"]) # Matches first pattern process_user_data(["admin", "system", "full"]) # Matches second pattern process_user_data({name: "Bob", role: "manager"}) # Matches third pattern process_user_data("invalid data") # Unmatched pattern Functional Ruby -- Keith Bennett -- Functional Conf 2025 24
  24. Hangout Room I will go to the hangout room after

    this presentation is over, in case anyone would like to discuss this subject further. Functional Ruby -- Keith Bennett -- Functional Conf 2025 25
  25. 26

  26. Keith R. Bennett Description Link Website https://www.bbs-software.com/ LinkedIn https://www.linkedin.com/in/keithrbennett/ Github

    https://github.com/keithrbennett Technical Blog https://blog.bbs-software.com/ TechHumans Blog https://techhumans.com/ StackOverflow https://stackoverflow.com/users/501266/keith-bennett Functional Ruby -- Keith Bennett -- Functional Conf 2025 27