Skip to content

Instantly share code, notes, and snippets.

@oNguyenNgocTrung
Created September 23, 2018 17:52
Show Gist options
  • Select an option

  • Save oNguyenNgocTrung/ab526c3152dd79d7405c29873e9a9acb to your computer and use it in GitHub Desktop.

Select an option

Save oNguyenNgocTrung/ab526c3152dd79d7405c29873e9a9acb to your computer and use it in GitHub Desktop.

Revisions

  1. Nguyen Ngoc Trung created this gist Sep 23, 2018.
    266 changes: 266 additions & 0 deletions block_ruby.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,266 @@
    ---
    layout: post
    title: Block trong Ruby
    tags: [ruby, block]
    ---

    ## Block là gì?

    Về cơ bản thì block là một hay nhiều dòng code được bao bởi `{}` hay `do``end`

    Về chức năng thì chúng như nhau, nhưng thường sẽ sử dụng `{}` cho block nào có ít code, thường là một dòng và ngược lại, nếu có nhiều code, thường hai dòng trở lên thì dùng `do ... end` mục đích chỉ để dễ đọc hơn thôi.

    Ví dụ khi viết code nhiều dòng

    ```ruby
    [1, 2, 3].each do |n|
    # Prints out a number
    puts "#{n}"
    end
    ```

    Chúng ta có thể viết thành 1 dòng như thế này

    ```ruby
    [1, 2, 3].each {|n| puts "#{n}"}
    ```

    `|n|` được gọi là `tham số block (block parameter)`, giá trị của nó trong trường hợp này được gọi lần lượt theo giá trị của mảng đã cho, đầu tiên nhận giá trị là 1, lần thứ 2 là 2, cuối cùng là 3

    Kết quả

    ```ruby
    1
    2
    3
    ```

    ## `yield` và cách sử dụng

    `yield` là một phần sức mạnh của `ruby block`, nó chịu trách nhiệm về mọi sự ảo diệu cũng như nhầm lẫn trong `block`. Phần lớn sự nhầm lẫn đều đến từ cách gọi `block` và việc làm thế nào để truyền tham số vào trong nó

    ```ruby
    def my_method
    puts "reached the top"
    yield
    puts "reached the bottom"
    end

    my_method do
    puts "reached yield"
    end
    ```

    Kết quả

    ```
    reached the top
    reached yield
    reached the bottom
    ```

    Về cơ bản, khi bạn thực thi `my_method` và đến dòng gọi đến `yield`, nó sẽ chuyển đến gọi block truyền tham số. Sau đoạn code bên trong block chạy xong, nó sẽ lại tiếp tục chạy code trong hàm `my_method` tiếp.

    ## `yield` có tác dụng thực thi code trong block, và bản thân nó sẽ nhận giá trị return từ block đó.

    ```ruby
    def test_yield
    puts "Start of method"
    puts yield
    puts "End of method"
    end

    test_yield {1 + 1}
    ```

    Kết quả

    ```
    Start of method
    2
    End of method
    => nil
    ```

    Ví dụ trên block `{1 + 1}` sẽ return về 2, `yield` thực thi code trong block nên sẽ nhận được return từ block là 2.

    `yield` cũng có thể truyền tham số vào.

    ```ruby
    def test_yield
    puts "Hello"
    yield "Foo", "Bar"
    end

    test_yield {|str1, str2, str3| puts str1 + str2}

    test_yield do |str1, str2|
    puts str1 + str2
    end
    ```

    ```
    Hello
    FooBar
    => nil
    ```

    Dễ dàng nhận ra, `| ... |` sẽ chứa danh sách tham số được sử dụng cho block, được truyền từ `yield` vào. Danh sách tham số trong `| ... |` sẽ map chính xác với danh sách tham số truyền vào khi gọi `yield`, gọi thừa hay thiếu cũng không sao, chỉ là không có dữ liệu cho tham số thôi.

    ```ruby
    def test_yield
    yield "a"
    end

    test_yield do |str1, str2|
    puts str1
    puts str2.nil?
    end
    ```

    kêt quả

    ```
    a
    true
    ```

    ```ruby
    def test_yield
    yield "a", "b"
    end

    test_yield do |str1|
    puts str1
    end
    ```

    Kết quả

    ```
    a
    => nil
    ```

    ## Sử dụng biến cục bộ trong block.

    Để khai báo biến cục bộ dùng trong block, trong danh sách tham số khai báo bởi `| ... |`, ta sẽ dùng `;` để chia danh sách tham số thành 2 phần, phần đầu là những tham số được truyền vào qua `yield`, phần còn lại phía sau là danh sách biến dùng cục bộ trong block.

    ```ruby
    x = 10
    y = 10
    1.times do |n; y|
    x = n
    y = n
    puts "x inside the block : #{x}"
    puts "y inside the block : #{y}"
    end
    puts "x outside the block : #{x}"
    puts "y outside the block : #{y}"
    ```

    Kết quả
    ```
    x inside the block : 0
    y inside the block : 0
    x outside the block : 0
    y outside the block : 10
    ```

    Ta dễ dàng thấy, biến `x` không được khai báo như biến local của block, nên giá trị của nó ở bên ngoài block đã bị thay đổi khi ta gán `x = n`, ngược lại giá trị của `y` vẫn không bị thay đổi bên ngoài block.

    ### Sử dụng `block_given? ` để kiểm tra có block nào được truyền vào method hay không.
    Một điều mà không nhắc cũng hiểu, đó là nếu block được truyền vào nhưng ta không gọi tới `yield` thì block đó cũng không có tác dụng gì và không hề được thực thi. Còn ngược lại, ta gọi `yield` nhưng không truyền block vào thì sao ?

    ```ruby
    def test_no_block
    yield
    end
    test_no_block
    ```
    ```
    LocalJumpError: no block given (yield)
    ```

    Sẽ có lỗi ngay lập tức. Để tránh lỗi xảy ra, ta có thể sử dụng method `block_given?`, method này sẽ kiểm tra xem có block nào được truyền vào method mà ta đang gọi `yield` hay không.

    ```ruby
    def test_block
    if block_given?
    yield
    else
    puts "No block given"
    end
    end

    test_block
    test_block {puts "I saw a block"}
    ```
    ```
    No block given
    I saw a block
    ```

    # Sử dụng `&block`
    Sử dụng cú pháp này để truyền reference của block như là một tham số vào trong method.

    ```ruby
    def test_method &block
    puts block
    block.call
    end

    test_method {puts "Hello Proc"}
    ```
    ```
    #<Proc:0x007f99e9896950@(pry):193>
    Hello Proc
    => nil
    ```

    Ta có thể thấy block ta truyền vào là một instance của class `Proc`.

    # Sử dụng `&:something`

    ```ruby
    ["a", "b"].map &:upcase
    ```
    ```
    => ["A", "B"]
    ```

    Như vừa nhắc tới cách sử dụng `&block`, sẽ không có chuyện gì nếu tham số truyền vào method là reference của một block, nhưng nếu nó không reference tới block thì method sẽ gọi tới `to_proc` để chuyển nó thành block cho việc sử dụng như bình thường.
    Như ví dụ trên, đầu tiên ta sẽ có `:upcase.to_proc` để tạo ra một instance của `Proc`, sau đó truyền reference của block vừa tạo ra vào trong method `.map` để sử dụng.

    # Ứng dụng
    Ứng dụng của block thì khỏi cần kể, dùng nó ở mọi lúc mọi nơi, nó cũng là một phần tạo ra sự đặc biệt cho ngôn ngữ Ruby.
    Tiện vừa nhắc tới việc nếu tham số truyền vào không phải reference tới một block, nó sẽ được `to_proc` để cố gắng chuyển thành một block. Ta có thể áp dụng nó cho bài toán cơ bản sau :
    > Từ một mảng các số nguyên, lọc ra những số chia hết cho 3.
    ```ruby
    class Fixnum
    def to_proc
    Proc.new do |obj|
    obj % self == 0
    end
    end
    end

    numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].select &3
    puts numbers
    ```
    ```
    3
    6
    9
    ```

    3 là một `Fixnum`, nên ta định nghĩa thêm method `to_proc` nhằm mục đích đưa logic để xác định nó có chia hết cho 3 hay không.

    # Kết luận
    `Block` là một trong những tính năng mạnh mẽ của ruby nhưng thường xuyên bị bỏ quên. Bạn có thể thấy `block` không chỉ đơn giản là một đoạn code giữa `do``end`, mà trong bài này ta còn được biết đến `yield`, nó cho phép bạn chèn các đoạn code vào bất cứ đâu trong phương thức. Điều đó có nghĩa là bạn có một phương thức mà có nhiều cách hoạt động khác nhau giống như có nhiều phương thức.

    ## Tài liệu tham khảo

    [Mastering ruby blocks in less than 5 minutes](https://mixandgo.com/blog/mastering-ruby-blocks-in-less-than-5-minutes)