30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

Ruby のモジュールの基礎

使用している Ruby のバージョンは2.5.3

モジュールの定義

以下の構文で定義する。

module モジュール名
end

モジュールはクラスと違い、インスタンスを作ることは出来ない。

module MyModule
end
MyModule.new # undefined method `new' for MyModule:Module (NoMethodError)

ミックスイン

クラスが、あるモジュールを取り入れてそのモジュールのメソッドを使えるようにすることを、ミックスインと呼ぶ。

includeでミックスインすると、モジュールで定義したメソッドをインスタンスメソッドとして使えるようになる。 ミックスインしたクラスのサブクラスでも、そのまま使える。

module SelfIntroduction
  def who_am_i(oneself)
    p "I'm #{oneself}."
  end
end

class Parent
  include SelfIntroduction

  def hello
    print 'Hello! '
    who_am_i self
  end
end

class Child < Parent
  def hey
    print 'Hey! '
    who_am_i self
  end
end

parent = Parent.new
parent.hello # Hello! "I'm #<Parent:0x00007ff97d895868>."
child = Child.new
child.hey # Hey! "I'm #<Child:0x00007ff97d895638>."

extendを使ってミックスインすると、モジュールのメソッドを、そのクラスの特異メソッド(クラスメソッド)として使うことが出来る。

module SelfIntroduction
  def who_am_i(oneself)
    p "I'm #{oneself}."
  end
end

class User
  extend SelfIntroduction
end

User.who_am_i(User) # "I'm User."

include されているモジュールを調べる

include?メソッドの引数にモジュールを渡すと、そのモジュールをincludeしているかどうかが返ってくる。
included_modulesメソッドは、includeしているモジュールを配列で返す。

module SelfIntroduction
end

class Parent
  include SelfIntroduction
end

class Child < Parent
end

p Parent.include?(SelfIntroduction) # true
p Child.include?(SelfIntroduction) # true

p Parent.included_modules # [SelfIntroduction, Kernel]
p Child.included_modules # [SelfIntroduction, Kernel]

Kernel とトップレベル

prequireといったメソッドは、Kernelというモジュールで定義されている。
そして、ObjectクラスがKernelincludeしているため、Objectを継承している全てのクラスで、pなどのメソッドを使える。

p Object.included_modules # [Kernel]

Ruby では、クラス構文やモジュール構文などで囲まれていない一番外側の部分を、「トップレベル」と呼ぶ。
そしてトップレベルのselfは、mainという名前のオブジェクトを指すが、これはObjectクラスのインスタンスである。トップレベルでpなどを使えるのは、このため。

p self # main
p self.class # Object
p self.class.include?(Kernel) # true

モジュールは Module クラスのインスタンス

モジュールは、Moduleクラスのインスタンスである。

module SelfIntroduction
end

p SelfIntroduction.class # Module

そして、Moduleクラスは、Objectクラスを継承している。

p Module.superclass # Object

ClassクラスはModuleクラスを継承している。
そのため、継承関係はClass -> Module -> Object -> BasicObjectになっている。

p Class.superclass # Module
p Module.superclass # Object
p Object.superclass # BasicObject

全てのクラスは、Classクラスのインスタンスでもある。

p Class.class # Class
p Module.class # Class
p Object.class # Class
p BasicObject.class # Class

ここらへんは自分でも理解できていない。循環参照のようになっていないか? ClassBasicObjectObjectを継承しているが、それらスーパークラスは、Classのインスタンスなのだから、Classから生まれてくる。だからまず先にClassが存在しているはず。しかしそのClassBasicObjectなどを継承しておかないといけない。Class定義時にはまだBasicObjectは存在しないにも拘わらず!
以下の状況が成立してしまうのがよく分からない。

p Module.instance_of?(Class) # true
p Class.superclass # Module

名前空間としてモジュールを使う

クラス定義をモジュール構文で囲うことで、名前空間を分けることが出来る。
参照する際はモジュール名::クラス名と記述する。

module Seller
  class Alice
    def role
      p 'I am seller.'
    end
  end
end

module Buyer
  class Alice
    def role
      p 'I am buyer.'
    end
  end
end

seller = Seller::Alice.new
seller.role # "I am seller."

buyer = Buyer::Alice.new
buyer.role # "I am buyer."

モジュールを入れ子にすることも出来る。

module User
  module PremiumUser
    class Alice
      def initialize
        p 'This is premium user.'
      end
    end
  end
end

User::PremiumUser::Alice.new # "This is premium user."

モジュールが既に定義済みの場合、class モジュール名::クラス名という構文でクラスを定義することも可能。

module Seller
end

class Seller::Alice
  def role
    p 'I am seller.'
  end
end

seller = Seller::Alice.new
seller.role # "I am seller."

トップレベルで定義しているクラスを明示的に呼び出す場合は、::クラス名と記述する。

class Alice
end

module Seller
  class Alice
    def initialize
      print Alice
      print ', '
      p ::Alice
    end
  end
end

Seller::Alice.new # Seller::Alice, Alice

関数や定数を提供するためにモジュールを使う

モジュールに特異メソッドを定義すれば、ミックスインすることなくそのメソッドを使える。

module SelfIntroduction
  def self.who_am_i(oneself)
    p "I'm #{oneself}."
  end
end

SelfIntroduction.who_am_i 1 # "I'm 1."

ミックスインとしても特異メソッドとしても使えるメソッドを、モジュール関数という。
モジュール関数はmodule_functionを使って定義する。
モジュール関数は自動的にprivateになるので、レシーバを指定して呼び出すことは出来ない。

module SelfIntroduction
  def who_am_i(oneself)
    p "I'm #{oneself}."
  end

  module_function :who_am_i
end

class User
  include SelfIntroduction

  def hello
    print 'Hello! '
    who_am_i self
  end
end

SelfIntroduction.who_am_i 1 # "I'm 1."
user = User.new
user.hello # Hello! "I'm #<User:0x00007fed11115930>."

user.who_am_i # private method `who_am_i' called for #<User:0x00007fed11115930> (NoMethodError)

モジュールは定数を定義することもでき、モジュール名::定数名で取得できる。

module SelfIntroduction
  SOME_VALUE = 'foo'
end

p SelfIntroduction::SOME_VALUE # "foo"

メソッド探索

クラスに対してancestorsメソッドを使うと、クラスやモジュールの配列が返ってくる。
クラスのインスタンスがメソッドを呼び出すとき、ancestorsの返り値の順番でメソッドを探索し、最初に見つかったメソッドを実行する。
最後まで見つからなかった場合はNoMethodErrorになる。

module A
end

class Parent
  include A

  def foo
    p "Parent's foo"
  end
  def bar
    p "Parent's bar"
  end
end

class Child < Parent
  def foo
    p "Child's foo"
  end
end

p Child.ancestors # [Child, Parent, A, Object, Kernel, BasicObject]
child = Child.new
child.foo # "Child's foo"
child.bar # "Parent's bar"

モジュールをincludeした場合、モジュールのメソッドより先にクラスのインスタンスメソッドを探索するが、prependメソッドでミックスインした場合は、ミックスインしたモジュールのメソッドを先に探索する。

module A
end

class Foo
  include A
end

class Bar
  prepend A
end

p Foo.ancestors # [Foo, A, Object, Kernel, BasicObject]
p Bar.ancestors # [A, Bar, Object, Kernel, BasicObject]

参考資料

gihyo.jp

useEffect の概要と async function を使う際の注意点

使用している React のバージョンは16.8.4

レンダー後の処理を指定するための仕組み

React Hooks の一つであるuseEffectは、レンダー後に実行したい処理を React に伝えるための仕組み。
useEffect(fn)と記述すると、DOMの更新が終わったあとにfnを実行する。
useEffectはレンダー後に必ず実行される。最初にレンダーした際もそうだし、propsstateに変更があってレンダーし直した際もそう。そこに区別はない。

以下の例では、このコンポーネントが表示された際にeffect!というログが流れる。
そしてボタンを押下した際にも、その都度effect!というログが流れる。

import React, {useState, useEffect} from 'react';

const App = () => {
  const [state, setState] = useState(0);

  useEffect(() => {
    console.log('effect!');
  });

  return (
    <>
      <div>{state}</div>
      <button
        type="button"
        onClick={() => {
          setState(state + 1);
        }}
      >
        increment
      </button>
    </>
  );
};
export default App;

公式ドキュメントではuseEffectの第一引数に渡している関数(上記の例ではconsole.log('effect!')を実行しているアロー関数)を「副作用関数」と呼んでいる(英語では単純にeffect)ので、ここでもそれに倣って副作用関数と呼ぶことにする。

副作用関数はレンダーする度に新しく作られる

副作用関数は、レンダーされる度に毎回新しく作られ、それが呼び出される。
そのため、そのときのコンポーネントの状態に応じて処理の内容を変える、ということが可能になる。

先程の例のconsole.logの部分を以下のように書き換えてみる。

console.log(state === 0 ? 'mounted!' : 'updated!');

こうすると、このコンポーネントが表示された際にmounted!とログに表示され、以降、ボタンを押下するたびにupdated!がログに表示される。
これは、レンダーする度に副作用関数が新しく作られることによって可能になっている。

このコンポーネントがマウントされたとき、以下の副作用関数が作られて実行される。
このときのstate0なので、こうなっている。

() => {console.log(0 === 0 ? 'mounted!' : 'updated!');}

そしてボタンを押すとstate1になり、DOMの更新が行われたあと、以下の副作用関数が作られて実行される。

() => {console.log(1 === 0 ? 'mounted!' : 'updated!');}

もう1度ボタンを押すとこう。

() => {console.log(2 === 0 ? 'mounted!' : 'updated!');}

つまり、公式ドキュメントにあるように「それぞれの副作用は特定のひとつのレンダーと結びついている」。

この仕組みを理解していないと、思った通りに副作用を実行できないことがある。

以下の例では、stateを1秒間隔でログに流す。

useEffect(() => {
  setInterval(() => {
    console.log(state);
  }, 1000);
});

マウント時にstate0の状態で実行されるので、0が表示され続ける。
ここでボタンを押してstate1になるとどうなるのかというと、1が毎秒流れるだけでなく、引き続き0もログに流れ続ける。

f:id:numb_86:20190321174823g:plain

なぜこうなるかというと、ボタンを押下した際に呼び出される副作用関数は、マウント時に呼び出された副作用関数とは何の関係もない独立した関数なので、以前に呼び出された副作用関数には何も影響を与えない。
そのため、上記の例だと、ボタンを押す度に新しくsetIntervalが実行される。
では古いsetIntervalをクリアするにはどうすればいいのか。

クリーンアップと呼ばれる機能を使うと、新しく副作用関数を実行する前に、前回実行した副作用関数の処理に影響を与えることが出来る。

クリーンアップは、新しく副作用関数を実行する前に呼び出される

副作用関数のなかで関数を返すと、それがクリーンアップのための関数になる。
この関数は、次に副作用関数が実行される際に、それに先立って呼び出される。

以下の例だと、console.log(`Previous state is ${state}.`);を実行しているアロー関数が、クリーンアップ。

const [state, setState] = useState(0);

useEffect(() => {
  console.log(`Current state is ${state}.`);
  return () => {
    console.log(`Previous state is ${state}.`);
  };
});

この場合、マウント時にCurrent state is 0.と表示される。
その後、ボタンを押すなどしてsetState(state + 1)を実行してstate1になると、次の副作用関数が呼ばれる前に、前回呼び出した副作用関数のクリーンアップが実行され、それから新しい副作用関数が呼ばれる。
そのため、まずPrevious state is 0.が表示され、そのあとにCurrent state is 1.が表示される。 以降、ボタンを押す度に同じ流れで処理が行われる。

先程のタイマーの例で言えば、以下のように書くことで、前回の副作用関数でセットしたタイマーがリセットされる。

useEffect(() => {
  const id = setInterval(() => {
    console.log(state);
  }, 1000);
  return () => {
    clearInterval(id);
  };
});

f:id:numb_86:20190321174857g:plain

副作用関数の実行をスキップする

副作用関数はレンダーされる度に必ず実行されるが、副作用の内容によっては毎回呼び出す必要がない、あるいは呼び出したくないケースもある。
その場合、useEffectの第二引数に配列を渡すことで、副作用関数を呼び出す条件を指定することが出来る。

useEffect(fn, [..deps])と記述すると、depsの内容を前回のfn実行時の内容と比較して、変化があったときにのみ再びfnを呼び出す。

以下の例では、inc stateボタンを押下しても副作用関数は実行されず、マウント時と、inc keyStateボタンを押下した場合にのみ、副作用関数が実行される。

import React, {useState, useEffect} from 'react';

const App = () => {
  const [state, setState] = useState(0);
  const [keyState, setKeyState] = useState(0);

  useEffect(() => {
    console.log('keyState has been incremented!');
  }, [keyState]);

  return (
    <>
      <div>{`${state}, ${keyState}`}</div>
      <button
        type="button"
        onClick={() => {
          setState(state + 1);
        }}
      >
        inc state
      </button>
      <button
        type="button"
        onClick={() => {
          setKeyState(keyState + 1);
        }}
      >
        inc keyState
      </button>
    </>
  );
};
export default App;

useEffectの第二引数の配列にkeyStateを渡している。
マウント時に副作用関数を実行するのはこれまで通りだが、そのときのkeyState0である。

次に、inc stateを押下する。そうするとstateが更新されたので再びレンダーする。
このとき React は副作用関数を実行しようとするが、その前にkeyStateの値をチェックする。前回は0だったが、今回も0である。そのため、配列の値に変化がないため、副作用関数は実行されない。

今度はinc keyStateを押下してみる。そうするとkeyStateがインクリメントされ、前回の値が0であったのに対して今回は1なので、副作用関数が実行される。

配列には複数の値を渡すことが可能で、どれか一つでも前回の値と違っていれば、副作用関数が実行される。

副作用関数のなかで非同期処理を行う際の注意点

副作用関数のなかで非同期処理を行う場合、処理の順序が担保されない可能性があることに注意する。

副作用関数はレンダーされる毎に実行するわけだが、以前実行した副作用関数の非同期処理のほうが解決に時間がかかった場合、意図しない動きになる可能性がある。

少し長いが、サンプルを貼る。

import React, {useState, useEffect} from 'react';

const fetchUser = id =>
  new Promise(resolve => {
    const responseTime = id === 1 ? 3000 : 1000;
    setTimeout(() => {
      resolve(`This is data ${id}`);
    }, responseTime);
  });

const App = () => {
  const [id, setId] = useState(null);
  const [message, setMessage] = useState('Please click button.');
  const [apiStatus, setApiStatus] = useState(null);

  useEffect(() => {
    if (id) {
      (async () => {
        const res = await fetchUser(id);
        setApiStatus(`complete (user is ${id})`);
        setMessage(res);
      })();
    }
  }, [id]);

  return (
    <>
      <div>{message}</div>
      <br />
      <div>API STATUS: {apiStatus}</div>
      <br />
      <button
        type="button"
        onClick={() => {
          setId(1);
        }}
      >
        Fetch data of user number 1.
      </button>
      <button
        type="button"
        onClick={() => {
          setId(2);
        }}
      >
        Fetch data of user number 2.
      </button>
    </>
  );
};
export default App;

fetchUserというAPIを叩く想定で、APIによってレスポンスの時間が異なる設定。
id1のときは3秒で、2のときは1秒でレスポンスが来る。

問題になるのは、1のAPIを叩いた直後に2のAPIを叩いたとき。
副作用関数の実行そのものは1のほうが早いが、APIのレスポンスを待っている場合に2の副作用関数が実行され、setMessageまで実行されてしまう。そしてその後で、1のレスポンスがようやく返ってきて、setMessageが実行される。
そのため、messageの最終的な値はThis is data 1になってしまう。

f:id:numb_86:20190321174930g:plain

これに対処するには、クリーンアップを上手く使うとよい。
今回のケースでは、useEffectを以下のように書き換える。

  useEffect(() => {
    let didCancel = false;

    if (id) {
      (async () => {
        const res = await fetchUser(id);
        setApiStatus(`complete (user is ${id})`);
        if (!didCancel) setMessage(res);
      })();
    }

    return () => {
      didCancel = true;
    };
  }, [id]);

didCancelの初期値はfalseなので、そのままならsetMessageは実行される。
だが、次の副作用関数が呼ばれると(今回のケースではid1から2に変わったタイミング)、前回の副作用関数のクリーンアップが実行され、前回の副作用関数におけるdidCanceltrueになる。
このため、APIのレスポンスがようやく返ってきてawait以降の処理を行う際に(!didCancel)の条件が満たされず、前回の副作用関数のsetMessageは実行されずに済む。

f:id:numb_86:20190321175002g:plain

参考資料