Okarin note

頭の整理もかねて色々と書いていきます

関数型プログラミングについてのまとめ

宣言型プログラミングというプログラミングパラダイムの一部

「何をするのか(What)」が重要で「どのようにするか(How)」は重要ではない。

結果を得るための手順を記述していく命令型プログラミングよりも何をしているかがわかりやすくプログラムの読解がしやすい。

ja.wikipedia.org

宣言型プログラミングの特徴

コードを見れば何をしているかがわかる。

実装に関する詳細は個々の関数によって抽象化され、隠蔽されている。

適切に分割、命令されて組み合わせることでコメントを書かなくてもやっていることがわかる。

関数型プログラミングの基本概念

イミュータブルなデータ

イミュータブルとは変更することができない状態を指す。

関数型では全てのデータがイミュータブル。

オブジェクトを操作するときは、直接参照を操作するのではなく、オブジェクトのコピーを作ってそれを操作しましょう。

TypeScript/JavaScriptだとスプレッド構文とかを使う。

純粋関数

引数の値のみを参照して、それを元に計算し、値を返す関数を「純粋関数」と言う。

純粋関数は以下の特徴がある。

  • 少なくとも一つの引数を取る
  • 値、もしくは関数を戻り値として返す
  • 副作用がない

純粋関数ではない例

const person = {
    name: 'okarin',
    canRead: false,
    canWrite: false
};

function selfEducate() { // 引数を受け取ってない
    person.canRead = true, // 関数外のオブジェクトに変更を与えるので副作用がある
    person.canWrite = true
} // 戻り値を返却しない

selfEducate();
console.log(okarin);

純粋関数である例

type Person = { name: string; canRead: boolean; canWrite: boolean };

const person: Person = {
    name: 'okarin',
    canRead: false,
    canWrite: false,
};

const selfEducate = (person: Person): Person => ({ // 引数を受け取る
    ...person, // スプレッド構文で元オブジェクトのコピーに対して操作しているので副作用がない
    canRead: true,
    canWrite: true,
}); // 戻り値を返却する

console.log(selfEducate(person)); // { name: 'okarin', canRead: true, canWrite: true }
console.log(person); // { name: 'okarin', canRead: false, canWrite: false }

高階関数

他の関数を引数に取るか、戻り値として関数を返すか、もしくはその両方を満たす関数のこと。

JavaScriptの関数は第一級オブジェクトなので関数を変数と同様にあつかうことができる。

そのためのこのような関数として扱える。

const invokeIf = (condition: boolean, fnTrue: () => void, fnFalse: () => void) => (condition ? fnTrue() : fnFalse());

const showWelcome = () => console.log('Welcome!!!!');
const showUnauthorized = () => console.log('Unauthorized!!!!');

invokeIf(true, showWelcome, showUnauthorized);
invokeIf(false, showWelcome, showUnauthorized);

再帰

関数の中から自分自身を再帰的に呼び出すテクニックのこと。

関数型において多用されている。

ループで書かれたコードは多くの場合は再帰で書きなおすことができ、コードが簡潔になります。

const countdown = (value: number, fn: (value: number) => void): number => {
    fn(value);

    return value > 0 ? countdown(value - 1, fn) : value;
};

console.log(countdown(10, (value) => console.log(value)));

関数型プログラミングで書いたデジタル時計

type ClockTime = {
    ampm: 'AM' | 'PM';
    hours: number;
    minutes: number;
    seconds: number;
};

const oneSecond = () => 1000;
const getCurrentTime = () => new Date();
const clear = () => console.clear();
const log = (message: any) => console.log(message);

const serializeClockTIme = (date: Date): ClockTime => ({
    ampm: 'AM',
    hours: date.getHours(),
    minutes: date.getMinutes(),
    seconds: date.getSeconds(),
});

const civilianHours = (clockTime: ClockTime): ClockTime => ({
    ...clockTime,
    hours: clockTime.hours > 12 ? clockTime.hours - 12 : clockTime.hours,
});

const appendAMPM = (clockTime: ClockTime): ClockTime => ({
    ...clockTime,
    ampm: clockTime.hours >= 12 ? 'PM' : 'AM',
});

const display = (target: any) => (time: ClockTime) => target(time);

const formatClock = (format: string) => (time: ClockTime) =>
    format
        .replace('hh', time.hours.toString())
        .replace('mm', time.minutes.toString())
        .replace('ss', time.seconds.toString())
        .replace('tt', time.ampm);

const prependZero = (key: keyof ClockTime) => (clockTime: ClockTime) => ({
    ...clockTime,
    [key]: clockTime[key] < 10 ? '0' + clockTime[key] : '' + clockTime[key],
});

const compose =
    (...fn: any) =>
    (arg: any) =>
        fn.reduce((composed: any, f: any) => f(composed), arg);

const convertToCivilianTime = (clockTime: ClockTime) => compose(appendAMPM, civilianHours)(clockTime);

const doubleDigits = (civilianTime: ClockTime) =>
    compose(prependZero('hours'), prependZero('minutes'), prependZero('seconds'))(civilianTime);

const startTicking = () => {
    setInterval(
        compose(clear, getCurrentTime, serializeClockTIme, convertToCivilianTime, doubleDigits, formatClock('hh:mm:ss tt'), display(log)),
        oneSecond()
    );
};

startTicking();

参考

Reactハンズオンラーニング www.oreilly.co.jp