どうも。つじけ(tsujikenzo)です。このシリーズでは2022年2月からスタートしました「ノンプロ研オブジェクト指向でなぜつくるのか第3版輪読会」についてお送りします。飛び入り参加しました、補章の資料です。
書籍について
今回は、こちらの書籍について、有志で輪読会を行いました。オブジェクト指向の基礎を学べる必読書だと思います。
初版は2004年です。時代の変化に合わせて改版が行われ、第3版では「関数型プログラミング」について補章が加わりました。
その、補章の発表を担当しましたので、資料をまとめたいと思います。
今日は1回目で、「関数型言語とラムダ式」 をお送りします。
今日のアジェンダ
今日のアジェンダはこちらです。
- 関数型言語とプログラミングパラダイム
- アルゴリズムを切り替える
- JavaのStrategyパターンとラムダ式
- JavaScriptのアロー関数
関数型言語とプログラミングパラダイム
プログラミング言語には、JavaやPythonなどのオブジェクト指向型言語のほかに、Haskell(ハスケル)やScala(スカラ)といった、関数型言語があります。
一方で、「関数型プログラミング」とは、プログラミングパラダイムのことで、プログラミングを書くときの考え方や、手法のことです。
現代のプログラミング言語(JavaやJavaScriptやPythonやC#など)は、マルチパラダイム言語なので、「オブジェクト指向言語だけど関数型プログラミングで書く」ということが可能です。
なので、「関数型プログラミングで書くためには、純粋関数型言語のHaskellで書かなければならない」という話ではありません。
書籍の意図としては、「関数型言語のHaskellを知り、みなさんがお使いの言語で、どのように関数型プログラミングを取り入れることができるのか考えてみましょう。」ということだと思います。
アルゴリズムを切り替える
Javaのポリモーフィズムは、「具体的な計算ロジックはあとから差し替え可能」というばめんで役に立ちます。
それを実現するために、「実装をもたない抽象メソッド」を、Interfaceクラス(抽象クラス)として宣言します。
そして、具体的な計算ロジックは、サブクラスでメソッドのオーバーライドとして記述します。
//実行エントリーポイント
public class Main {
public static void main(String[] args) {
Trainer trainer = new Trainer();
trainer.excute(new Baby()); // おぎゃー
trainer.excute(new Dog()); // ワンワン
}
}
//Animalを鳴かすトレーナークラス
class Trainer {
void excute(Animal animal) {
System.out.println(animal.cry());
}
}
//実装をもたない抽象メソッドをもつ抽象クラス
abstract class Animal {
abstract String cry();
}
//抽象クラスを継承して、実装するサブクラス1
class Baby extends Animal {
//オーバーライドによる実装
String cry() {
return "おぎゃー";
}
}
//抽象クラスを継承して、実装するサブクラス2
class Dog extends Animal {
//オーバーライドによる実装
String cry() {
return "ワンワン";
}
}
JavaのStrategyパターンとラムダ式
この、「アルゴリズムをごっそり切り替えたい」という、設計手法の1つに「Strategyパターン」があります。
Strategyパターンは、3つのクラスで構成されます。
クラス | 概要 | 例 |
---|---|---|
Strategy | 抽象的な戦略 | Animalクラス |
ConcreateStrategy | 具体的な戦略 | Dog,Babyクラス |
Context | Strategy役を利用するクラス (ConcreteStrategy役のインスタンスを持っている) | Trainerクラス |
UMLはこちら。
出典: 「Strategy パターン」フリー百科事典『ウィキペディア(Wikipedia)』
「A,B,Cから呼び出された」という別々の計算ロジック(ConcreateStrategy)を、インターフェイス(Strategy)と、コンテキスト(Context)で記述したコードがこちらです。
//MainApp test application
public class MainApp {
public static void main(String[] args) {
Context context;
// 異なるアルゴリズムに従う3つのコンテキスト。
context = new Context(new ConcreteStrategyA());
context.execute();
context = new Context(new ConcreteStrategyB());
context.execute();
context = new Context(new ConcreteStrategyC());
context.execute();
}
}
//ConcreteStrategy を指定して作成され、Strategy オブジェクトへの参照を保持する。
class Context {
Strategy strategy;
// Constructor
public Context(Strategy strategy) {
this.strategy = strategy;
}
public void execute() {
this.strategy.execute();
}
}
//具体的な戦略を実装するクラスは、このインターフェイスを実装する。
//コンテキストクラスは、具体的な戦略を呼び出すためにこのインターフェイスを使用する。
interface Strategy {
void execute();
}
//Strategy インターフェイスを用いたアルゴリズムの実装。
class ConcreteStrategyA implements Strategy {
public void execute() {
System.out.println("Called ConcreteStrategyA.execute()");
}
}
class ConcreteStrategyB implements Strategy {
public void execute() {
System.out.println("Called ConcreteStrategyB.execute()");
}
}
class ConcreteStrategyC implements Strategy {
public void execute() {
System.out.println("Called ConcreteStrategyC.execute()");
}
}
このStrategyパターンは、プログラマの強力な武器になりますが、「具体的な計算ロジックが1行だけ」というばあいでも、都度クラスを作成しないといけないことがネックでした。
そこで、インターフェイスを型とした変数に、具体的な計算ロジックを代入できるようにしたのが、「ラムダ式」です。
原文を、このように置き換えることができます。
//原文
interface Strategy {
void execute();
}
class ConcreteStrategyA implements Strategy {
public void execute() {
System.out.println("Called ConcreteStrategyA.execute()");
}
}
//ラムダ式による書き換え
Strategysa= () -> System.out.println("Called ConcreteStrategyA.execute()");
全体をリファクタリングしてみると、かなりのコード量を削減できていることがわかると思います。
//MainApp test application
public class MainApp {
public static void main(String[] args) {
Context context;
// Strategyインターフェイスを型とした変数に、具体的な計算ロジックを代入する
Strategy sa= () -> System.out.println("Called ConcreteStrategyA.execute()");
Strategy sb= () -> System.out.println("Called ConcreteStrategyB.execute()");
Strategy sc= () -> System.out.println("Called ConcreteStrategyC.execute()");
// 異なるアルゴリズムに従う3つのコンテキスト。
context = new Context(sa);
context.execute();
context = new Context(sb);
context.execute();
context = new Context(sc);
context.execute();
}
}
//ConcreteStrategy を指定して作成され、Strategy オブジェクトへの参照を保持する。
class Context {
Strategy strategy;
// Constructor
public Context(Strategy strategy) {
this.strategy = strategy;
}
publicvoidexecute() {
this.strategy.execute();
}
}
//具体的な戦略を実装するクラスは、このインターフェイスを実装する。
//コンテキストクラスは、具体的な戦略を呼び出すためにこのインターフェイスを使用する。
interface Strategy {
void execute();
}
このように、実装が必要なメソッドを1つだけもつインターフェイスを 「関数型インターフェイス」 と呼びます。
JavaScriptのアロー関数
関数型プログラミングで使われる「ラムダ式」は、JavaScriptのアロー演算子「=>」として理解している方も多いと思います。
function sample1() {
//あえて省略記法を用いておりません
constfunc = (value) => { return value };
console.log(func("hoge")); //hoge
}
最大の特徴は、関数を値として扱い、引数に渡したり変数に代入できる、ということです。
これは、関数が「第一級オブジェクトである」という、言語の仕組みからきています。
みなさんも、if文やfor文を変数に代入して、失敗した経験があるのではないでしょうか。
文(statement)と式(expression)の違いを理解しておきましょう。
function sample2() {
//文(statement)は変数に代入できない
// const x = if (0 < 1) { console.log("trueです"); }
//三項演算子は演算子で評価された式(expression)
const y = 0 < 1 ? console.log("trueです") : ''; // trueです ←真式の値を返す
console.log(y); //undefined ←returnが無いため
}
アロー関数の省略記法
アロー関数の省略記法は、覚えてしまえば怖くありません。丸暗記しましょう。
function sample3() {
//引数がひとつの時はカッコを省略できる
const getFullName = name => {
const fullName = name + '様';
return fullName;
};
console.log(getFullName('soichiro'));
//引数がない時はカッコを省略できない
constgetToday = () => {
const today = newDate();
return today;
};
console.log(getToday());
//1ライナーで書けるときは、retrurnと{}を省略できる
constgetMyBirthday = bd => newDate(bd);
const birthDay = '1980/04/29';
console.log(getMyBirthday(birthDay));
}
※メソッド定義には、アロー関数は使いませんのでご注意ください。
まとめ
以上で、「関数型言語とラムダ式」をお送りしました。
GASを学んでいるかたは、とりあえずアロー関数の記述方法をマスターすることが、関数型プログラミングを習得する第一歩だと思います。
また、JavaのStrategyパターンからポリモーフィズムの具体的な書き方をまなび、ご自身の言語に取り入れてみましょう。
次回は、「パターンマッチングと再帰」 をお届けします。
参考資料
- オブジェクト指向でなぜつくるのか 第3版 平澤 章著 日経BP
- Strategy パターン Wikipedia
- Java言語で学ぶデザインパターン入門第3版 結城 浩著 SBクリエイティブ
- 徹底攻略Java SE 11 Silver問題集[1Z0-815]対応 志賀 澄人著 インプレス