座標変換

座標変換

学習演習

はじめに

クロージャは、JavaScriptのプログラミングパターンのひとつで、外側のレキシカルスコープにある変数を、入れ子になったコードブロックの内側で使えるようにするものです。 JavaScriptはクロージャを意識せずに使えるようにしていて、それが何なのかを知らないまま使われていることもよくあります。

// Top-level declarations are global-scope
const dozen = 12;

{
  // Braces create a new block-scope
  // Referencing the outer variable is a closure.
  const twoDozen = dozen * 2;
}

// Because of the block-scope declaration, twoDozen is not available here.
twoDozen;
// => Uncaught ReferenceError: twoDozen is not defined

波括弧({})のほかにも、関数(やクラス)は新しいスコープを作り、値を_包み込む_ことができます。

const dozen = 12;

// Functions create a new function-scope and block-scope.
// Referencing the outer variable here is a closure.
function nDozen(n) {
  // This is declared inside the function scope, and uses the top-level scope.
  // This works, and encloses the value 12.
  const twoDozen = dozen * 2;

  // This only uses the locally declared variable and the passed argument to the parameter `n`
  return (twoDozen / 2) * n;
}

// Because of the function-scope declaration, twoDozen is not available here.
twoDozen;
// => Uncaught ReferenceError: twoDozen is not defined

twoDozenの例からわかるように、値は_入れ子_になったスコープ(関数、ブロックなど)に包み込むことはできますが、そのコンテキストの外へ取り出すことはできません。 ほとんどの場合、モダンなJavaScriptでは、値が外側のスコープへ_漏れない_ことが意図されています。

状態を保ち、値を受け渡すクロージャ

再代入できる変数宣言(letやvarなど)を使うと、ある程度の状態を保持できます。

let counter = 0;

// This function closure increments the counter's state in the outer lexical context.
// This way the counter can be shared between many calling contexts.
export function increment() {
  counter += 1;
  return counter;
}

increment();
// => 1

counter;
// => 1

状態を漏らさずに値を包み込む

値を使って状態を保つことと、包み込んだ値が外に漏れないことを組み合わせると、プライベートな値を作れます。

もっとも一般的な方法は、何らかの状態を包み込んだ関数を返す関数を作ることです。

export function makeCounter() {
  let counter = 0;

  // This returns a new function that encloses the local variable counter
  return function increment() {
    counter += 1;
    return counter;
  };
}

// Counter did not leak
counter;
// => Uncaught ReferenceError: counter is not defined

// This creates a new counter.
// This assigns the increment function to the variable myFirstCounter.
const myFirstCounter = makeCounter();

typeof myFirstCounter;
// => function

myFirstCounter.name;
// => increment

myFirstCounter();
// => 1
myFirstCounter();
// => 2

// This creates new counter (with new, separate local state / enclosed counter variable)
const mySecondCounter = makeCounter();

mySecondCounter();
// => 1

// It does not affect the first counter.

myFirstCounter();
// => 3
Note

多くのプログラマーにとって、クロージャは難しい概念です。また、関数から関数を返すことは、すべてのプログラミング言語で一般的とは限りませんし、そもそもできない言語もあります。 もっと読みたい場合は、MDNのクロージャのガイドがとても詳しいです。

説明

デザイン会社では、これまで主にCSS変換を使ってWebページを作ってきました。話し合いの結果、一部の計算をJavaScriptで動的に行うことにしました。チームメイトの中にはJavaScriptの経験が浅い人もいるため、関数クロージャを使い、{x, y}の座標ペアを繰り返し変換できるようにすることにしました。

1. 座標を平行移動する

translate2d関数を実装しましょう。この関数はクロージャを利用して、座標ペアの2D平行移動を繰り返し実行する関数を返します。

幾何学において、平行移動とは、点やベクトル、図形を同じ方向に同じ距離だけ動かすことを指します。 これは、すべての点に定数を足すことだと解釈できます。

const moveCoordinatesRight2Px = translate2d(2, 0);
const result = moveCoordinatesRight2Px(4, 8);
// result => [6, 8]

2. 座標を拡大縮小する

scale2d関数を実装しましょう。この関数はクロージャを利用して、座標ペアの2D拡大縮小を繰り返し実行する関数を返します。

幾何学において、一様な拡大縮小とは、ベクトルや図形を同じ方向に拡大または縮小することを指します。 これは、すべての点に定数(拡大率)を掛けることだと解釈できます。

この演習では、正の拡大率のみを扱うものとします。

const doubleScale = scale2d(2, 2);
const result = doubleScale(6, -3);
// result => [12, -6]

3. 変換関数を組み合わせる

2つの変換関数を組み合わせて、繰り返し実行できる変換を作ります。これは一般に_関数合成_と呼ばれ、1つ目の関数_'f(x)'の結果を2つ目の関数'g(x)'_の入力として使います。

const moveCoordinatesRight2Px = translate2d(2, 0);
const doubleCoordinates = scale2d(2, 2);
const composedTransformations = composeTransform(
  moveCoordinatesRight2Px,
  doubleCoordinates,
);
const result = composedTransformations(0, 1);
// result => [4, 2]

4. 関数の結果を保存する

memoizeTransform関数を実装しましょう。この関数は_メモ化_する関数を受け取り、渡された関数への入力を覚えておく新しい関数を返します。これにより、最後の戻り値を「記憶」しておき、同じ引数で再び呼び出されたときは1回だけ計算すればよくなります。

メモ化は、_動的計画法_で使われることがあります。 結果が記憶されるため、処理の重い操作を1回だけ実行すればよくなります。 注意: この演習では、_すべて_の結果をメモ化する動的計画法の一部の解法とは異なり、最後の結果だけを記憶します。

const tripleScale = scale2d(3, 3);
const memoizedScale = memoizeTransform(tripleScale);

memoizedScale(4, 3); // => [12, 9], this is computed since it hasn't been computed before for the arguments
memoizedScale(4, 3); // => [12, 9], this is remembered, since it was computed already
GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
JavaScript Exercism

座標変換を始める準備はできましたか?

Exercismに登録すれば、37個のコンセプト159個の演習、そして本物の人間によるメンタリングとともに、JavaScriptを学んでマスターできます。すべて無料です。