Help us understand the problem. What is going on with this article?

クロージャを使った問題解決

はじめに

クロージャ(Closure)の概念を初めて知った時の感想は「何に使うんだこれ?」でした。それ以来、色々な言語でクロージャのサンプルを実装したりしながらも、実際にクロージャを活用して綺麗に問題を解決するということはありませんでした。
それがつい先日、実務の中でちょうど良い問題をクロージャで解決できました。これが自分なりに少し嬉しかったので、クロージャの簡単な解説を交えながらご紹介したいと思います。

クロージャとは

クロージャとは、誤解があるかもしれませんが、「自分の外に、自分以外がアクセスできない変数を持った関数」と理解しています。具体的に、簡単なクロージャを作ると下のようになります。

$ ts-node
> function createClosure() {
    const str = "hello world"
    return () => {
      console.log(s)
    }
  }
undefined
> createClosure()
[Function]
> createClosure()()
hello world
undefined

createClosure 関数は無名関数を返しており、この無名関数がまさにクロージャです。このクロージャは内部に変数を持たず、自分の外側にある str という変数にアクセスしています。この str という変数にはクロージャ自身(と createClosure 関数)しかアクセスできないようになっています。

このように、関数であるにもかかわらず状態を保持し、擬似的な Private 変数を扱うことができるのがクロージャ、と自分なりに理解しています。
(厳密には、クロージャは関数でなくても定義可能であったり、あるいはHaskellといった純粋関数型言語にもクロージャの概念が導入できるようです)

クロージャを用いた問題解決の具体例:

仕事でWebフロントページの開発をしている際に、「全ての項目を選択するボタン」を実装したのですが、そこでクロージャを活用しました。今回はその時の実装を参考に、簡略化したアプリケーションとして再現しました。

「すべてを選択するボタン」の実装

sample.gif

画面のイメージは図のようになります。最近はまっている筋トレを題材にしました。
画面に表示するデータはAPI経由で取得している都合上、

  • 項目の数が動的に増減する
  • 項目が複数カテゴリに分けられているため「全ての項目を選択する」ボタンの数自体も動的に増減させる

という仕様を満たす必要がありました。「全ての項目を選択する」ボタンが1つであればどう実装してもシンプルになりそうですが、動的な内容が多かったためクロージャの威力が発揮されたように思います。

「すべてを選択するボタン」とクロージャの相性が良い点

「すべてを選択するボタン」は 1.チェックがない状態からチェックを付けられるパターン と 2.チェックが付けられた状態からチェックを外されるパターン でそれぞれ動作が異なります。

image.png

つまり、「すべてを選択するボタン」は、一度呼び出されると全項目を追加し、次に呼び出されると全項目を削除し、次に呼び出されると全項目を追加し、、、これはまさにクロージャで表現できる機能ではないでしょうか!

実際のコード

このように、「全項目を追加したり、全項目を削除する」という処理を check という名前のクロージャとして実装し、これを allChecker オブジェクトに持たせるようにしました。check クロージャの内部には「チェックが付けられているかどうか」を表す状態を持たせています。処理と状態を合わせた、まさにクロージャとして活用しています。

また、上半身、下半身などカテゴリごとに「全てを選択するボタン」を用意するので、同じ数だけ allChecker オブジェクトを用意します。

allChecker オブジェクトを作成するのは createAllChecker 関数が担います。

@click="allCheckers[part.bodyPartsName].check()
この1行にある通り、全てを選択するのラジオボタンをクリックする度に、allChecercheck 関数が呼び出されています。check 関数は1回呼び出される度に処理を変えるクロージャです。

今回は動的な要素が多かったため、これをクロージャなしで実装すると余分な変数やマッチング処理がかなり増えてしまうのではないかなと思います。

ということで、最終的なコードは下のようになりました。サンプルアプリの全コードはこちらに上げてあります。
kazukiyoshida/sample-allchecker-vue

<template lang="pug">
.div.allWrap
  p >> AllCheckers
  p {{ this.allCheckers }}
  p >> 選択された筋肉(部位ごと)
  p {{ this.checkedMusclesByParts }}
  template(v-for="part in this.menu")
    .partWrap
      span.part 【部位】{{ part.bodyPartsName }}
      p
        input(
          type="checkbox"
          @click="allCheckers[part.bodyPartsName].check()"
        )
        span すべての筋肉を選択
        template(v-for="muscle in part.muscles")
          p
            input(
              type="checkbox"
              :value="muscle"
              v-model="checkedMusclesByParts[part.bodyPartsName]"
            )
            span {{ muscle }}
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import _ from 'lodash'
import {
ITrainingMenu
} from '../interfaces/menu'

@Component
export default class extends Vue {

  // API経由で取得したデータは store に保存してあるので、そこからデータを取得
  public get menu(): ITrainingMenu[] | null {
    return this.$store.state.training.menu
  }

  // 選択された筋肉の一覧
  public checkedMusclesByParts = {}

  //「すべての筋肉を選択する」を実行する allChecker を、全ての部位について集めたもの
  public allCheckers = {}

  // 「すべての筋肉を選択する」を実行する allChecker を作成する.
  public createAllChecker(partsName: string) {
    // 選択されているかどうかのフラグ
    let isChecked = false
    // 指定したパーツにおける全ての筋肉のリスト
    const allMuscles = _.find(this.menu, ['bodyPartsName', partsName]).muscles

    return {
      // check 関数はクロージャ
      check: (): void => {

        // チェックなし -> チェックあり:要素の追加
        if (!isChecked) {
          const diff = _.difference(
            allMuscles,
            this.checkedMusclesByParts[partsName]
          )
          this.checkedMusclesByParts[partsName].push(...diff)

        // チェックあり -> チェックなし:要素の削除
        } else {
          this.checkedMusclesByParts[partsName] = _.without(
            this.checkedMusclesByParts[partsName],
            ...allMuscles
          )
        }

        // 最後にチェックをつける/はずす
        isChecked = !isChecked
      }
    }
  }

  // ライフサイクル
  public async mounted() {
    // トレーニングデータを取得
    await this.$store.dispatch('training/fetchTrainingMenu', {})

    _.forEach(this.menu, (parts) => {
      // allChecker を部位ごとに作成
      this.$set(
        this.allCheckers,
        parts.bodyPartsName,
        this.createAllChecker(parts.bodyPartsName)
      )

      // 「選択された筋肉一覧」を部位ごとに作成
      this.$set(
        this.checkedMusclesByParts,
        parts.bodyPartsName,
        []
      )
    })
  }
}
</script>

おわりに

非常にニッチな例でしたが、具体的なクロージャを用いた問題解決の例をご紹介しました。
まだまだ勉強中ですので、ご指摘ご意見などありましたらコメントいただけると幸いです。

最後まで目を通していただきありがとうございます。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした