Is that a question?

本文同步刊錄在 Medium

什麼是複合/合成函數(function composition)?

來看看維基百科的定義: Wikipedia describes function composition as follows:

In computer science, funciton composition is an act or mechanism to combine simple functions to build more complicated ones. Like the usual composition of functions in mathematics, the result of each function is passed as the argument of the next, and the result of the last one is the result of the whole.

在數學中,合成函數是把兩個函數鏈接在一起的過程,內層函數的輸出就是外層函數的輸入

在程式語言中,一個方法(Method, 或稱函式 Function)的回傳值作為另一個方法的參數(parameter)傳入,如此串接直到最後一個方法的輸出即是整個 Function composition 的輸出結果。

  • Example function:
# double the number
def double(x)
  x * 2
end

double(2) # => 4
double(3) # => 6
double(4) # => 8

# squares the number
def square(x)
  x * x
end

square(2) # => 4
square(3) # => 9
square(4) # => 16

同時因為這些方法只單純的計算傳入的引數(argument),沒有其他副作用,故如果想要將一個數字先乘以二再平方,我們可以直接組合這兩個方法:

square(double(2)) = 16

為了方便呼叫,將上述操作指定給一個新的方法:

double_then_square(x) = square(double(x))
double_then_square(2) # => 16
double_then_square(3) # => 36

某些程式語言中有所謂『一級公民』 “first-class” 的概念,讓我們不必先宣告一個全新的方法才能來組合他們。

在 Functional programming 中大量用到一種系列的方法來處理問題,有了這個 first-class function composition 的概念,可以讓其更容易的將某些肥厚的方法抽象化分離成更小的元件。

Functions in Ruby

如上所述,我們 Rubyist 亦需要一種方法,可以將程式碼片段當作引數傳給其他方法、儲存變數或資料結構,甚至需要它作為回傳值從其他方法中 return。

換言之,我們需要 “first-class functions” 來做到這件事。
其中一個明確的例子就是 Proc。

Proc

根據 Ruby documentation 的描述:Proc 物件是一段封裝過的程式碼區塊,可以被拿來做各種快樂的事。

A Proc object is an encapsulation of a block of code, which can be stored in a local variable, passed to a method or another Proc, and can be called. Proc is an essential concept in Ruby and a core of its functional programming features.

Ruby 作為一個物件化相當徹底的語言,所見的幾乎所有東西都是物件,包含數字等等,除了其中一個例外:Block

Block 是一小段程式碼區塊,既沒有自己的名字亦無法單獨存在,很可憐。

平常像寄生蟲一樣必須得依附在一個宿主身上,由宿主的行為決定要不要執行它,如果沒人操作它便會沈入記憶體之海。

這時可透過創造一個 Proc 物件把這段程式碼區塊承接下來(物件化),等待適合的時機呼叫它。

# Use the Proc class constructor
double = Proc.new { |number| number * 2 }

# Use the Kernel#proc method as a shorthand
double = proc { |number| number * 2 }

# Receive a block of code as an argument (note the &)
def make_proc(&block)
  block
end

double = make_proc { |number| number * 2 }
def make_proc
  Proc.new
end

double = make_proc { |number| number * 2 }

呼叫 Proc 物件有以下幾種方法:

double.call(2) # => 4
double.(2)     # => 4
double[2]      # => 4
# shorthand of double.===(2)
double === 2   # => 4

值得注意的是第四個呼叫的方法,因為 Case statement 在執行判斷比較條件的時候也是透過 “===” 關聯運算子(relationship operator)同時檢查複數的條件,所以在這種情況特別有用。

以經典的 Fizz Buzz 題目舉例:

# 定義一個 Porc 物件把 block 包起來,當呼叫的時候會在裡面做運算並回傳布林值(true or false)
divisible_by_15 = proc { |number| (number % 15).zero? }
divisible_by_5 = proc { |number| (number % 5).zero? }
divisible_by_3 = proc { |number| (number % 3).zero? }

num = 9

case num
# when 會呼叫 divisible_by_15 的 === 方法,並把 num 當引數傳進去
# divisible_by_15 === 9
when divisible_by_15
  puts "FizzBuzz"
when divisible_by_5
  puts "Buzz"
when divisible_by_3
  puts "Fizz"
else
  puts num
end
# Fizz

Lambda

和 Proc 長得很像,用法也很像,兩者僅有微小的差別:

  • Lambda 比較類似 function ,對於傳入的引數數量很嚴格。
  • 呼叫 return 會回傳結果然後脫離 lambda 。
double = lambda { |number| number * 2 }
double = -> (number) { return number * 2 }
double[2] # => 4
double[2, 3] # => ArgumentError (wrong number of arguments (given 2, expected 1))
  • Proc 的表現如同block,對傳入的引數並不介意,在裡面呼叫 return 會爆炸
double_p = proc { |number| return number * 2 }
double_p[2] # => LocalJumpError (unexpected return)

Method

在 Ruby 我們能用上的另一個特色就是透過 Method 將方法包成物件。

class Greeter
  attr_reader :greeting

  def initialize(greeting)
    @greeting = greeting
  end

  def greet(subject)
    "#{greeting}, #{subject}!"
  end
end

greeter = Greeter.new("Hello")
greet   = greeter.method(:greet)

也一樣可以透過呼叫 Proc 的方法使用它:

p greet.call("world") # => "Hello, world!"
p greet.("world")     # => "Hello, world!"
p greet["world"]      # => "Hello, world!"
p greet === "world"   # => "Hello, world!"

To Proc or not to Proc:

透過 to_proc 可以將一個方法轉為 Proc 物件:

def my_even?
  :even?.to_proc
end

my_even?[3] # => false

另外我們可以透過 & 將方法作為 Proc 物件傳給另一個方法,這也是我最愛的功能!

# 列出 1 到 10 的偶數陣列
list = 1.upto(10).select(&:even?)
# 此式等價於:
# list = 1.upto(10).select(&:even?.to_proc)

list # => [2, 4, 6, 8, 10]

Ruby 提供了 >>(forward composition) 和 <<(backward composition) 方法將各種 Proc 組合起來,可以很直覺的看作是執行的順序,熟悉了之後就可以這樣用:

double_then_square = square << double
double_then_square[2] # => 16

square_then_double = square >> double
square_then_double[2] # => 8
(square >> double)[2] # => 8

又或者:

to_camel    = :capitalize.to_proc
add_header  = -> val { "Title: " + val }
strip_space = :strip.to_proc

format_as_title = add_header << to_camel << strip_space

format_as_title["  \thello world\n"]
# "Title: Hello world"

paragraph break

維持簡潔的同時,可讀性提高了不少,
希望 Ruby 的美也能讓更多人欣賞到。

參考資料:

⤧  Previous post 最龐克的 VS Code 主題: Make your editor rock in a Synthwave way! ⤧  Next post [Algorism] 找出規律的藝術:Codewars 6 kyu - Simple Fun 116: Prime String