Rubyでの楽しい関数型プログラミング



この記事では、関数型プログラミング、プログラミングインターフェースのシンプルさと設計についてさらに学ぶために、Rubyの縮退したフォームを介した目的のない旅に焦点を当てています。



コードを表現する唯一の方法はラムダ式であり、利用可能なデータ構造は配列のみであるとします。



square = ->(x) { x * x } square.(4) # => 16 person = ["Dave",:male] print_person = ->((name,gender)) { puts "#{name} is a #{gender}" } print_person.(person)
      
      





これらは関数型プログラミングの非常に基本的なものです。関数は私たちが持っている唯一のものです。 同じスタイルで実際のコードに似たものを書いてみましょう。 あまり苦痛を伴わずにどこまで行けるか見てみましょう。



人に関する情報を含むデータベースを使用したい場合、誰かが内部ストレージとやり取りするためのいくつかの機能を提供してくれたとします。 ユーザーインターフェイスと入力検証を追加します。



これが、リポジトリへの連絡方法です。



 insert_person.(name,birthdate,gender) # =>  id update_person.(new_name,new_birthdate,new_gender,id) delete_person.(id) fetch_person.(id) # =>  ,       
      
      





最初に、データベースに人を追加できる必要があります。 この場合、入力データを検証する必要があります。 このデータを標準入力ストリームから抽出します( gets



およびputs



は組み込み関数であり、期待どおりに機能すると想定しています)。



 puts "Name?" name = gets puts "Birthdate?" birthdate = gets puts "Gender?" gender = gets
      
      







データを検証してデータベースに追加する関数が必要です。 彼女はどのように見えますか? 人物の属性を受け入れ、検証と挿入が成功した場合はid



を返し、何か問題が発生した場合はエラーメッセージを返します。 例外やハッシュテーブル(配列のみ)がないため、創造的に考える必要があります。



アプリケーションでは、すべてのビジネスロジックメソッドが2つの要素の配列を返すことに同意しましょう。最初の要素は正常に完了したときの関数の値であり、2番目の要素はエラーメッセージを含む文字列です。 配列のセルの1 nil



nil



)が存在するかどうかは、操作の成功または失敗を示します。



何が受け入れられ、何が返される必要があるかがわかったので、関数自体の記述を開始します。



 add_person = ->(name,birthdate,gender) { return [nil,"Name is required"] if String(name) == '' return [nil,"Birthdate is required"] if String(birthdate) == '' return [nil,"Gender is required"] if String(gender) == '' return [nil,"Gender must be 'male' or 'female'"] if gender != 'male' && gender != 'female' id = insert_person.(name,birthdate,gender) [[name,birthdate,gender,id],nil] }
      
      





String()



わからない場合、 nil



渡されると、この関数は空の文字列を返します。



ユーザーが次のような正しいデータを提供するまで、この関数をループで使用します。



 invalid = true while invalid puts "Name?" name = gets puts "Birthdate?" birthdate = gets puts "Gender?" gender = gets result = add_person.(name,birthdate,gender) if result[1] == nil puts "Successfully added person #{result[0][0]}" invalid = false else puts "Problem: #{result[1]}" end end
      
      





もちろん、サイクルを使用できないとは言いませんでした:)しかし、サイクルがないと仮定します。



ループは単なる関数です(再帰的に呼び出されます)



ループするには、単にコードを関数にラップし、目的の結果が得られるまで再帰的に呼び出します。



 get_new_person = -> { puts "Name?" name = gets puts "Birthdate?" birthdate = gets puts "Gender?" gender = gets result = add_person.(name,birthdate,gender) if result[1] == nil puts "Successfully added person #{result[0][0]}" result[0] else puts "Problem: #{result[1]}" get_new_person.() end } person = get_new_person.()
      
      





コードにif result[1] == nil



ような多くのチェックがあると想定できるので、それらを関数でラップしましょう。 関数の素晴らしいところは、ロジックではなく構造を再利用できることです。 ここでの構造は、エラーをチェックし、成功または失敗時に2つの関数のいずれかを呼び出すことです。



 handle_result = ->(result,on_success,on_error) { if result[1] == nil on_success.(result[0]) else on_error.(result[1]) end }
      
      





get_new_person



関数は、より抽象的なエラー処理メソッドを使用するようになりました。



 get_new_person = -> { puts "Name?" name = gets.chomp puts "Birthdate?" birthdate = gets.chomp puts "Gender?" gender = gets.chomp result = add_person.(name,birthdate,gender) handle_result.(result, ->((id,name,birthdate,gender)) { puts "Successfully added person #{id}" [id,name,birthdate,gender,id] }, ->(error_message) { puts "Problem: #{error_message}" get_new_person.() } ) } person = get_new_person.()
      
      





handle_result



使用handle_result



と、配列のインデックス付けを使用する代わりに、変数に明示的に名前をhandle_result



ことができhandle_result



。 これで、フレンドリ名error_message



使用できるだけでなく、配列を部分に「分割」し、フォームの構文を使用して個別の関数パラメーターとして使用することもできます((id,name,birthdate,gender))







これまでのところ良い。 このコードは少し奇妙に見えるかもしれませんが、冗長でわかりにくいものではありません。



その他の機能-よりクリーンなコード



私たちのコードのどこにも、「人」のデータ構造の正式な定義がなかったことは珍しいように思えます。 配列があり、最初の要素は名前、2番目は生年月日などであることに同意しました。アイデアは非常に単純ですが、新しいフィールド:titleを追加する必要があると想像してみましょう。 これを実行しようとすると、コードはどうなりますか?



これで、データベースはinsert_person



およびupdate_person



新しいバージョンを提供します。



 insert_person.(name,birthdate,gender,title) update_person.(name,birthdate,gender,title,id)
      
      





add_person



メソッドを変更しadd_person







 add_person = ->(name,birthdate,gender,title) { return [nil,"Name is required"] if String(name) == '' return [nil,"Birthdate is required"] if String(birthdate) == '' return [nil,"Gender is required"] if String(gender) == '' return [nil,"Gender must be 'male' or 'female'"] if gender != 'male' && gender != 'female' id = insert_person.(name,birthdate,gender,title) [[name,birthdate,gender,title,id],nil] }
      
      





新しいフィールドを使用しているため、 get_new_person



更新する必要がありget_new_person



。 エヘム:



 get_new_person = -> { puts "Name?" name = gets.chomp puts "Birthdate?" birthdate = gets.chomp puts "Gender?" gender = gets.chomp puts "Title?" title = gets.chomp result = add_person.(name,birthdate,gender,title) handle_result.(result, ->((name,birthdate,gender,title,id)) { puts "Successfully added person #{id}" [id,name,birthdate,gender,title,id] }, ->(error_message) { puts "Problem: #{error_message}" get_new_person.() } ) }
      
      





これは、アプリケーションコンポーネントの強力な接続性の本質を示しています。 get_new_person



は、特定のレコードフィールドについてまったく心配する必要はありません。 関数はそれらをadd_person



てからadd_person



渡すだけadd_person



。 いくつかの新しい関数でコードを取り出した場合、これを修正する方法を見てみましょう。



 read_person_from_user = -> { puts "Name?" name = gets.chomp puts "Birthdate?" birthdate = gets.chomp puts "Gender?" gender = gets.chomp puts "Title?" title = gets.chomp [name,birthdate,gender,title] } person_id = ->(*_,id) { id } get_new_person = -> { handle_result.(add_person.(*read_person_from_user.()) ->(person) { puts "Successfully added person #{person_id.(person)}" person }, ->(error_message) { puts "Problem: #{error_message}" get_new_person.() } ) }
      
      





これで、個人に関するデータの保存方法に関する情報は、 read_person_from_user



person_id



2つの関数に隠されています。 レコードにフィールドを追加する場合、 get_new_person



を変更する必要はありません。



コードが煩わしい場合は、以下に簡単な説明を示します。 *



配列を引数のリストとして渡すことができ、その逆も可能です。 person_id



、パラメーターリスト*_, id



を使用します。これは、最後の引数を除くすべての引数を_



配列に入れるようにRubyに指示し(この配列には関心がないため、その名前があります)、最後をid



変数に入れます。 これはRuby 1.9でのみ機能します。 1.8では、最後の引数のみが*



構文を使用できます。 次に、 add_person



を呼び出すときに、 read_person_from_user



の結果で*



を使用します。 read_person_from_user



は配列を返すため、 add_person



は明示的な引数を受け入れるため、この配列を引数リストとして使用します。 これがまさに*



機能です。 いいね!



コードに戻ると、 read_person_from_user



person_id



非常に強力に接続されてperson_id



ことがread_person_from_user



ます。 どちらもデータの保存方法を知っています。 さらに、データベースからのデータを処理するための新しい機能を追加した場合、配列の内部構造についても知っている関数を使用する必要があります。



何らかのデータ構造が必要です。



データ構造は単なる関数です



通常のRubyでは、この時点ですでにクラスまたは少なくともHash



編成していましたが、それらを使用することはできません。 関数のみで実際のデータ構造を作成できますか? 最初の引数をデータ構造の属性と見なす関数を作成する場合、可能です。



 new_person = ->(name,birthdate,gender,title,id=nil) { return ->(attribute) { return id if attribute == :id return name if attribute == :name return birthdate if attribute == :birthdate return gender if attribute == :gender return title if attribute == :title nil } } dave = new_person.("Dave","06-01-1974","male","Baron") puts dave.(:name) # => "Dave" puts dave.(:gender) # => "male"
      
      





new_person



はコンストラクターとして機能しますが、オブジェクト(返されない)を返す代わりに、呼び出されたときに特定のレコードのさまざまな属性の値を返すことができる関数を返します。



クラスで同じ動作を実装する場合と比較してください。



 class Person attr_reader :id, :name, :birthdate, :gender, :title def initialize(name,birthdate,gender,title,id=nil) @id = id @name = name @birthdate = birthdate @gender = gender @title = title end end dave = Person.new("Dave","06-01-1974","male","Baron") puts dave.name puts dave.gender
      
      





面白い。 これらのコードのサイズはほぼ同じですが、クラスのあるバージョンは特別な構造を使用しています。 特別な構造は、本質的にプログラミング言語が提供する魔法です。 このコードを理解するには、次のことを知る必要があります。





機能バージョンを理解するには、次のことを知っておく必要があります。





私が言ったように、それは私にとって興味深いようです。 基本的に同じことを行うには2つの方法がありますが、1つは他の方法よりもはるかに専門的な知識が必要です。



さて、実際のデータ構造ができました。 配列ではなくコードで動作するようにコードを変更しましょう:



 read_person_from_user = -> { puts "Name?" name = gets.chomp puts "Birthdate?" birthdate = gets.chomp puts "Gender?" gender = gets.chomp puts "Title?" title = gets.chomp new_person.(name,birthdate,gender,title) } add_person = ->(person) { return [nil,"Name is required"] if String(person.(:name)) == '' return [nil,"Birthdate is required"] if String(person.(:birthdate)) == '' return [nil,"Gender is required"] if String(person.(:gender)) == '' return [nil,"Gender must be 'male' or 'female'"] if person.(:gender) != 'male' && person.(:gender) != 'female' id = insert_person.(person.(:name),person.(:birthdate),person.(:gender),person.(:title)) [new_person.(person.(:name),person.(:birthdate),person.(:gender),person.(:title),id),nil] } get_new_person = -> { handle_result.(add_person.(read_person_from_user.()), ->(person) { puts "Successfully added person #{person.(:id)}" person }, ->(error_message) { puts "Problem: #{error_message}" get_new_person.() } ) }
      
      





add_person



は、属性を取得するための構文が原因であまり美しくありませんが、プログラムの構造を維持しながら、新しいフィールドを簡単に追加できます。



オブジェクトの方向は単なる機能です。



派生フィールドを作成することもできます。 タイトルを指定したユーザーに挨拶を追加するとします。 これを属性にすることができます。



 new_person = ->(name,birthdate,gender,title,id) { return ->(attribute) { return id if attribute == :id return name if attribute == :name return birthdate if attribute == :birthdate return gender if attribute == :gender return title if attribute == :title if attribute == :salutation if String(title) == '' return name else return title + " " + name end end nil } }
      
      





地獄、私たちがしたい場合は、実際のOOPスタイルのメソッドを追加できます



 new_person = ->(name,birthdate,gender,title,id) { return ->(attribute) { return id if attribute == :id return name if attribute == :name return birthdate if attribute == :birthdate return gender if attribute == :gender return title if attribute == :title if attribute == :salutation if String(title) == '' return name else return title + " " + name end elsif attribute == :update update_person.(name,birthdate,gender,title,id) elsif attribute == :destroy delete_person.(id) end nil } } some_person.(:update) some_person.(:destroy)
      
      





OOPについて話している間に、継承を追加しましょう! 人間の従業員がいるが、従業員番号はま​​だあるとします。



 new_employee = ->(name,birthdate,gender,title,employee_id_number,id) { person = new_person.(name,birthdate,gender,title,id) return ->(attribute) { return employee_id_number if attribute == :employee_id_number return person.(attribute) } }
      
      





数行のコードで、関数のみのクラス、オブジェクト、および継承を作成しました。



ある意味では、オブジェクト指向言語のオブジェクトは、共通のデータセットにアクセスできる関数のセットです。 関数型言語にオブジェクトシステムを追加することが、関数型言語を理解している人にとって些細なことであると考えられる理由は簡単にわかります。 これは、オブジェクト指向言語に機能を追加するよりもはるかに簡単です!



属性にアクセスするための構文はあまり美しくありませんが、クラスがなくてもひどい苦痛はありません。 クラスは、真面目な概念というよりも構文糖衣のように見えます。



ただし、データの変更には問題が発生する場合があります。 add_person



関数の詳細をご覧ください。 彼女はinsert_person



を呼び出してレコードをデータベースに入れ、IDを取得します。 次に、IDを設定するために、まったく新しいレコードを作成する必要があります。 古典的なOOPでは、単にperson.id = id



書きます。



状態遷移はこの設計の利点ですか? このデザインのコンパクトさがその主な利点であり、デザインをコンパクトにするのはオブジェクトの可変性であるという事実は単なる偶然です。 極端なメモリ不足とひどいガベージコレクションを備えたシステムを使用する場合にのみ、新しいオブジェクトの作成を心配できます。 ゼロからの新規および新規オブジェクトの無用な作成に本当に悩まされます。 ただし、関数に関数を追加する方法は既にわかっているので、このコンパクトな構文を実装してみましょう。



 new_person = ->(name,birthdate,gender,title,id=nil) { return ->(attribute,*args) { return id if attribute == :id return name if attribute == :name return birthdate if attribute == :birthdate return gender if attribute == :gender return title if attribute == :title if attribute == :salutation if String(title) == '' return name else return title + " " + name end end if attribute == :with_id # <=== return new_person.(name,birthdate,gender,title,args[0]) end nil } }
      
      





これで、 add_person



さらにシンプルになりました。



 add_person = ->(person) { return [nil,"Name is required"] if String(person.(:name)) == '' return [nil,"Birthdate is required"] if String(person.(:birthdate)) == '' return [nil,"Gender is required"] if String(person.(:gender)) == '' return [nil,"Gender must be 'male' or 'female'"] if person.(:gender) != 'male' && person.(:gender) != 'female' id = insert_person.(person.(:name),person.(:birthdate),person.(:gender),person.(:title)) [new_person.(:with_id,id),nil] # <==== }
      
      







もちろん、これはperson.id = id



ほどきれいに見えませんが、読みやすいほどまともです。 このコードは改善されました。



名前空間は単なる関数です



私が本当に恋しいのは名前空間です。 Cをプログラミングしたことがある場合は、名前の競合を避けるために、複雑なプレフィックスを持つ関数でコードが散らばる方法をご存じでしょう。 もちろん、ここで似たようなことをすることもできますが、RubyのモジュールやJavaScriptのオブジェクトリテラルを提供する名前空間など、適切な名前空間を用意する方がずっと良かったでしょう。 言語に新しい機能を追加せずにこれを実行したいと思います。 最も簡単な方法は、マッピングのようなものを実装することです。 データ構造の明示的な属性には既にアクセスできているため、これを行うためのより一般的な方法を考えれば十分です。



現時点では、唯一のデータ構造は配列です。 クラスがないため、配列メソッドはありません。



Rubyの配列は実際にはタプルであり、それらに対して実行できる最も一般的な操作はデータ抽出です。 例:



 first = ->((f,*rest)) { f } # or should I name this car? :) rest = ->((f,*rest)) { rest }
      
      





キー、値、表示の残りの3つの要素を含むリストと見なして、表示をリストとしてモデル化できます。 「OOPスタイル」を避け、純粋な「機能主義」のみを残しましょう。



 empty_map = [] add = ->(map,key,value) { [key,value,map] } get = ->(map,key) { return nil if map == nil return map[1] if map[0] == key return get.(map[2],key) }
      
      





使用例:



 map = add.(empty_map,:foo,:bar) map = add.(map,:baz,:quux) get.(map,:foo) # => :bar get.(map,:baz) # => :quux get.(map,:blah) # => nil
      
      





これは名前空間を実装するのに十分です:



 people = add.(empty_map ,:insert ,insert_person) people = add.(people ,:update ,update_person) people = add.(people ,:delete ,delete_person) people = add.(people ,:fetch ,fetch_person) people = add.(people ,:new ,new_person) add_person = ->(person) { return [nil,"Name is required"] if String(person.(:name)) == '' return [nil,"Birthdate is required"] if String(person.(:birthdate)) == '' return [nil,"Gender is required"] if String(person.(:gender)) == '' return [nil,"Gender must be 'male' or 'female'"] if person.(:gender) != 'male' && person.(:gender) != 'female' id = get(people,:insert).(person.(:name), person.(:birthdate), person.(:gender), person.(:title)) [get(people,:new).(:with_id,id),nil] }
      
      





もちろん、 new_person



の実装をディスプレイに置き換えることもできますが、サポートする属性の明示的なリストがある方が便利なので、 new_person



はそのままにしてnew_person



ます。



最後のトリック。 include



は、明示的な名前空間解決を使用しないように、モジュールを現在のスコープに取り込むことができる優れたRuby機能です。 ここでできますか? 閉じる:



 include_namespace = ->(namespace,code) { code.(->(key) { get(namespace,key) }) } add_person = ->(person) { return [nil,"Name is required"] if String(person.(:name)) == '' return [nil,"Birthdate is required"] if String(person.(:birthdate)) == '' return [nil,"Gender is required"] if String(person.(:gender)) == '' return [nil,"Gender must be 'male' or 'female'"] if person.(:gender) != 'male' && person.(:gender) != 'female' include_namespace(people, ->(_) { id = _(:insert).(person.(:name), person.(:birthdate), person.(:gender), person.(:title)) [_(:new).(:with_id,id),nil] } }
      
      





まあ、それはすでに多すぎるかもしれませんが、関数を使用するだけで同じ動作を実現できる場合、印刷を減らすためだけにinclude



を使用することinclude



依然として興味深いです。



私たちは何を学びましたか?



ほんのいくつかの基本的な言語構成要素を使用して、新しいプログラミング言語を作成することができました。 実際のデータ型、名前空間を作成でき、言語構成子による明示的なサポートなしでOOPを使用することもできます。 そして、Rubyの組み込みツールのみを使用しているかのように、ほぼ同じ量のコードでこれを行うことができます。 構文は通常のRubyよりも少し冗長ですが、それでもそれほど悪くはありません。 この「クロップされた」バージョンのRubyを使用して実際のコードを記述することもできます。 そして、それは絶対にひどく見えなかっただろう。



毎日の仕事に役立ちますか? これはシンプルさの教訓だと思います。 Rubyは高度に特殊化された構造、複雑な構文、メタプログラミングでオーバーロードされていますが、クラスを使用しなくても多くのことを実装できました! たぶんあなたの問題はもっと簡単な方法で解決できますか? たぶん、あなたはすべての「最もクールな機能」を使用しようとするよりも、言語の最も明らかな部分に頼るべきでしょうか?



All Articles