Methods và block trong Ruby

Method và block trong ruby

I. Method

Trước khi nói về methods tôi sẽ sơ lược một chút về object.

  • object là gì: nó là một instance của class, trong nó có chứa instance variables và instance methods

    Ví dụ sau bạn sẽ thấy rõ:

        class Student
            def info
                @name = "I'm Ruby"
            end
        end
    2.1.5 :010 > st.methods.grep /info/
    => [:info]
    2.1.5 :008 > st.instance_variables
    => [:@name]

Theo ví dụ ở trên thì stlà một object của class Student.

1. Instance method

  • Là method của instance object, có thể gọi nó thông qua object. Như ví dụ trên ta có thể nói info là một method của st (hay chính xác hơn nó là một instance method của class Student).
    2.1.5 :012 >Student.instance_methods.grep /info/
 => [:info]
 2.1.5 :013 > st.info
 => "I'm Ruby"

Nhưng không được nói info là một method của Student như vậy sẽ bị nhầm sang class method.

 2.1.5 :015 > st.methods == Student.instance_methods
 => true
 2.1.5 :016 > Student.methods.grep /info/
 => []

object's method không "sống" trong object mà nó "sống" trong clas của object, chính vì thế mà nó được gọi là instance method của class.

2. Method Lookup

Trước khi đi vào method look ta cần hiểu về hai khái niệm receiverancestors chain

  • receiver là gì? Đơn giản nó chỉ là một object và gọi được phương thức từ nó. Như trên thì st là một receiver
  • ancestors chain: trước khi đi vào định nghĩa tôi có một ví dụ sau:
    class Animal
        def weight
            puts "10kg"
        end
    end

    class Cat < Animal
    end
    2.1.5 :025 > ca = Cat.new
    => #<Cat:0x000000022bec18>
    2.1.5 :026 > ca.weight
    10kg

Khi gọi ca.weight, ruby sẽ tìm trong class ca instance method weight nhưng không thấy, tiếp tục tìm đến superclass tức là class mà nó kế thừa (ở đây là Animal), sau khi tìm thấy nó sẽ dừng lại và thực thi method. Nhưng giả sử nó không tìm thấy ở class nó kế thừa Animal nó sẽ tiếp tục tìm lên superclass của class Animal là Object. Path từ object -> class -> superclass -> superclass... như vậy được gọi là ancestors chain

    2.1.5 :033 > ca.class.ancestors
    => [Cat, Animal, Object, Kernel, BasicObject]

method_lookup.png

Vậy có thể kết luận như sau: Method lookup là việc tìm kiếm phương thức, Ruby sẽ tìm trong class của receiver và từ đó nó sẽ tìm method theo ancesstors chain cho đến khi nó tìm được phương thức cần gọi.

Ngoài ra ancesstors chain còn bao gồm cả module

module M
    def name
        "Reck"
    end
end

class Dog
    include M
end
2.1.5 :034 > dg = Dog.new
 => #<Dog:0x0000000144a1f0>
2.1.5 :035 > dg.name
 => "Reck"
2.1.5 :036 > dg.class.ancestors
 => [Dog, M, Object, Kernel, BasicObject]

3. Thực thi Method

Sau khi đã tìm được phương thức theo ancesstor chain Ruby sẽ thực thi phương thức mà nó tìm được.

Khi gọi một method thì receiver sẽ trở thành self (curent object) tất cả các instance variables của receiver sẽ là instance của self, và tất cả các phương thức gọi không tường minh bên trong nó tương đương với việc gọi self.my_instance_method.

class MyClass
    def testing_self arg
        @var = 10 # 1 biến instance của self
        my_instance_method(arg) # giống như gọi self.my_instance_method(arg)
    end
    def my_instance_method arg
        @var = @var + arg
    end
end

4. Dynamic Method

a. Gọi phương thức động

Thông thường khi gọi một phương thức bạn thường sử dụng object + dấu chấm + phương thức , ngoài ra thì có thể gọi phương thức thông qua cách khác đó là sử dụng phương thức send của Object Theo ví dụ MyClass trên ta có thể gọi được instance method của nó thông qua send

    2.1.5 :010 >  my = MyClass.new
     => #<MyClass:0x00000001cc2cd8>
    2.1.5 :011 > my.testing_self 1
     => 11
    2.1.5 :012 > my.send(:my_instance_method, 10)

Tham số đầu tiên trong phương thức send là tên phương thức, tham số sau là tham số của phương thức. Phương thức send này rất hữu ích trong việc tránh trùng lặp code, làm cho code của bạn DRY hơn, để thấy được điều đó bạn xem ví dụ tiếp theo:

    class Animal
        def action arg
            if arg == "run"
                self.run
            elsif arg = "eat"
                self.eat
            end
        end

        def run
            "Runing..."
        end

        def eat
            "Eating..."
        end
    end

Và giờ bạn refactoring nó với phương thức send

    class Animal
        def action arg
            self.send(arg)
        end

        def run
            "Runing..."
        end

        def eat
            "Eating..."
        end
    end

Bạn thấy đó send đã giải quyết vấn đề if else làm cho code của bạn DRY hơn.

Chú ý thêm là send có thể nhận tên phương thức truyền vào theo hai kiểu string và symbol.

b. Định nghĩa một phương thức động

Bạn định nghĩa dynamic method phương thức define_method()

    class Cat
        define_method :fingers do |arg|
            arg * 4
        end
    end
2.1.5 :018 > ca = Cat.new
 => #<Cat:0x00000001aa9370>
2.1.5 :019 > ca.fingers 1
 => 4

define_mehtod() được thực thi bên trong class Cat và nó được định nghĩa như một instance methods của class Cat, công nghệ định nghĩa phương thức tại thời điểm runtime như vậy được gọi là Dynamic Method.

    class Animal
        def self.define_action name
            define_method(name) do
                "#{name.capitalize}ing..."
            end
        end
        define_action :run
        define_action :eat
        define_action :drink
    end
2.1.5 :021 >   a = Animal.new
 => #<Animal:0x00000002caf470>
2.1.5 :022 > a.run
 => "Runing..."
2.1.5 :023 > a.eat
 => "Eating..."

define_method() được thực thi bên trong class Animal do đó để định nghĩa dynamic method trong một phức thức khác thì ta phải định nghĩa nó trong một class method. Việc định nghĩa dynamic method một cách hợp lý sẽ giúp code của bạn ngắn gọn hơn rất nhiều.

**5. Method missing **

Khi bạn gọi một phương thức, ruby sẽ tìm phương thức dựa trên ancesstor chain, nếu không tìm thấy phương thức nó sẽ gọi đến phương thức method_missing() và rais ra message NoMethodError,

2.1.5 :025 > a.walk
NoMethodError: undefined method `walk' for #< Animal:0x00000002caf470>

Bạn có thể override lại phương thức method_missing để custom tùy theo ý mình

class Animal
    def method_missing(method, *args)
        puts "Method #{method} that you've called does not exist"
    end
end
2.1.5 :040 > a = Animal.new
 => #<Animal:0x00000002b0c438>
2.1.5 :041 > a.lfkajds
Method lfkajds that you've called does not exist

Giờ ta sẽ refactoring lại class Animal mà sử dụng define_method phía trên

    class Animal
        def method_missing(method_name, *args)
            "#{method_name.capitalize}ing..."
        end
    end
2.1.5 :047 > a = Animal.new
 => #<Animal:0x00000002cb2f08>
2.1.5 :048 > a.eat
 => "Eating..."

Giờ bạn có thể gọi các phương thức drink, eat, run như bạn đã làm đối với define_method. Lúc này drink, eat, run ... được gọi là Ghost Method bởi nó chỉ được gọi ở phía người gọi mà bạn không tìm thấy nó được ở receiver.

Hãy so sánh số dòng code của class Animal ở hai cả 3 cách viết send, define_method và method_missing rõ ràng là method_missing đã ngắn hơn rất nhiều.

II. Block

Block là một phần của "callable object(bao gồm procs, lambda ...)", nó được định nghĩa với ngoặc xoắn hoặc keyword do .... end, bạn chỉ có thể định nghĩa một block khi bạn gọi phương thức và phương thức có thể gọi ngược trở lại block thông qua từ khóa yeild.

class Animal
    def super_animal arg1, arg2
        yield(arg1, arg2)
    end
end
2.1.5 :013 > a.super_animal(2, 2){|x, y| x*x + y*y}
 => 8

Nếu sử dụng yield trong method mà lúc gọi ta không định nghĩa block cùng method sẽ sinh ra lỗi.

2.1.5 :014 > a.super_animal(2, 2)
LocalJumpError: no block given (yield)
def super_animal arg1, arg2
    yield(arg1, arg2) if block_given?
    return "plz define your block"
end
2.1.5 :024 > a.super_animal(2,3)
 => "plz define your block"

Tips:

2.1.5 :045 >arr = [1,2,3,4]
 => [1, 2, 3, 4]
2.1.5 :046 >     arr.map(&:to_s)
 => ["1", "2", "3", "4"]

Việc gọi map bên trên tương đương với:

2.1.5 :009 > arr.map do |ar|
2.1.5 :010 >     ar.to_s
2.1.5 :011?>   end
 => ["1", "2", "3", "4"]

Proc

  • Proc là một block, bạn có thể định nghĩa một proc với từ khóa Proc.new và kết hợp với block theo sau nó, nó là một anonymous functions
  • Proc là block rồi tại sao lại cần đến nó là vì khi nào bạn muốn tái sử dụng block thì định nghĩa một proc và call nó khi nào cần thiết ngoài ra thì bạn cũng có thể sử dụng proc một cách trực tiếp ngay khi định nghĩa proc
2.1.5 :012 > pr = Proc.new{|name| "Hello #{name}" }
 => #<Proc:[email protected](irb):12>
2.1.5 :013 > pr.call("Ruby")
 => "Hello Ruby"

Lambda

  • Lambda cũng giống như một proc
2.1.5 :014 > la = lambda{|x| "I'm a #{x}"}
 => #<Proc:[email protected](irb):14 (lambda)>
2.1.5 :015 > la.call("Lambda")
 => "I'm a Lambda"

Có 2 điểm khác biệt giữa proc và lambda

  • Về tham số truyền vào: proc sẽ không check số tham số truyền vào ngược lại lambda thì có check:
    2.1.5 :016 > la.call("Lambda", "San")
ArgumentError: wrong number of arguments (2 for 1)
	from (irb):14:in `block in irb_binding'
	from (irb):16:in `call'
	from (irb):16
	from /home/khanh/.rvm/rubies/ruby-2.1.5/bin/irb:11:in `<main>'

Trong khi đó không có lỗi sinh ra với proc

2.1.5 :017 > pr.call("aa", "bb")
 => "Hello aa"

Về return và giá trị trả về: Khi return trong proc thì mehod mà gọi proc sẽ dừng và trả về giá trị nhưng đối với lambda thì không khi gặp phải return nó sẽ trả về giá trị và method chứa nó vẫn tiếp tục chạy. Ví dụ dưới đây sẽ cho bạn thấy rõ điều đó

    2.1.5 :001 > class Animal
    2.1.5 :002?>   def pr_ex
    2.1.5 :003?>     Proc.new{return "Proc val"}.call
    2.1.5 :004?>     puts "return value pr_ex"
    2.1.5 :005?>     end
    2.1.5 :006?>   def ld_ex
    2.1.5 :007?>     lambda { return "lambda" }.call
    2.1.5 :008?>     puts "return value ld_ex"
    2.1.5 :009?>     end
    2.1.5 :010?>   end
     => :ld_ex
    a = Animal.new
    2.1.5 :014 > a.pr_ex
     => "Proc val"
    2.1.5 :015 > a.ld_ex
    return value ld_ex
     => nil

Như bạn thấy rõ ràng từ hai method trên thì đối với proc nó sẽ không chạy lệnh puts "return value pr_ex" nhưng lambda thì lại khác nó tiếp tục chạy dòng lệnh puts "return value ld_ex"