かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

JavaScript 高階関数 (higher-order function) と カリー化 (currying)

今まで意識せずに使ってたけど、ジムでランニングしながら見てた funfunfunction で面白かったのでメモ。

高階関数 higher-order function

関数を引数に取る関数のことを、高階関数 (higer-order function) というらしい。
Javascript だと callback 関数とかで割と見かけるやつの事だった。

高階関数のメリット

  • ロジックを分離して再利用可能にする
  • ロジックが分離されるので、処理部分のコードが短く見やすくなる

例えば、次のリストから type"cool" のアイドルだけを取り出したいような場合

const idols = [
  { name: '星宮 いちご',   type: 'cute' },
  { name: '霧矢 あおい',   type: 'cool' },
  { name: '紫吹 蘭',      type: 'sexy' },
  { name: '有栖川 おとめ', type: 'pop' },
  { name: '藤堂 ユリカ',  type: 'cool' },
  { name: '神谷 しおん',  type: 'cool' },
  { name: '一ノ瀬 かえで', type: 'pop' },
  { name: '三ノ輪 ヒカリ', type: 'sexy' },
  ...
];

通常の for文 の場合、要件は満たせるがロジックが分離されてないのでここ限りで再利用することは難しい

const coolIdols = [];
for (var i = 0, l = idols.length; i < l; i += 1) {
  if ( idols[i].type === 'cool' ) {
    coolIdols.push(idols[i]);
  }
}
console.log(coolIdols);
// => [{ name: '霧矢 あおい', type: 'cool' }, { name: '藤堂 ユリカ', type: 'cool' }, ...]

高階関数である Array.filter を使うと次のような感じで書ける。

const coolIdols = idols.filter((idol) => {
  return idol.type === 'cool';
});
console.log(coolIdols);
// => [{ name: '霧矢 あおい', type: 'cool' }, { name: '藤堂 ユリカ', type: 'cool' }, ...]

filter に渡している関数は idol.type === 'cool' を判定するだけなので、配列をループさせる処理とは切り離されている。
判定しているロジックを別の関数に切り出すと、判定するだけの関数として再利用が可能になる。

// filterの引数に渡す関数を分離する
const isCoolIdol = function(idol) {
  return idol.type === 'cool';
};

const coolIdols = idols.filter( isCoolIdol );
// => [{ name: '霧矢 あおい', type: 'cool' }, { name: '藤堂 ユリカ', type: 'cool' }, ...]

例えば配列から条件にマッチしている要素を除くfilterを作成しすると…
Array-reject.js

Array.prototype.reject = function(func) {
  const res = [];
  if ( typeof(func) !== 'function' ) {
    return this;
  }
  this.forEach(function(v) {
    if ( !func(v) ) {
      res.push(v);
    }  
  });
  return res;
}

👇 同じ判定ロジックで、別の条件のデータを作成できたりする

require('./Array-reject');

// cool type かどうかを判定するロジック
const isCoolIdol = function(idol) {
  return idol.type === 'cool';
};

// type "cool" の idols リスト
const coolIdols = idols.filter( isCoolIdol );
// => [{ name: '霧矢 あおい', type: 'cool' }, { name: '藤堂 ユリカ', type: 'cool' }, ...]

// type "cool" 以外の idol リスト
const notCoolIdols = idols.reject( isCoolIdol );
// => [{ name: '星宮 いちご', type: 'cute' }, { name: '紫吹 蘭', type: 'sexy' },...]

ロジックを使い回すことができ、処理をする部分のコードも短くなって何をしているかの見通しが良くなる!
ってのが高階関数のメリットってことみたい。

カリー化 currying

名称は知ってたけどどういうものか知らず、なんで?🍛って思ってたやつ。

複数の引数をとる関数を、引数が「もとの関数の最初の引数」で戻り値が「もとの関数の残りの引数を取り結果を返す関数」であるような関数にすること(あるいはその関数のこと)である。
cf. https://ja.wikipedia.org/wiki/%E3%82%AB%E3%83%AA%E3%83%BC%E5%8C%96

という事らしい。
雑に言えば、「引数を取って、その引数を使った関数を返す関数」ということっぽい

e.g.

function over(standard_value) {
  return function(x) {
    return x > standard_value;
  }
}

パラメーターに応じて判定の変わる関数を返す関数

const over5 = over(5);
console.log( over5(10) );   // => true
console.log( over(5)(10) ); // => true

返される関数の内部に最初に渡された引数を持っているので、動的に関数を作り出せる (= カリー化)

console.log( over(2)(5) );  // => true
const over10 = over(10);
console.log( over10(5) );  // => false
console.log( over10(11) ); // => true

ということっぽい。

高階関数とカリー化を組み合わせる

高階関数に渡す関数をカリー化すると更に便利になる

e.g.

function over(standard_value) {
  console.log('over init!');
  return function(x) {
    return x > standard_value;
  }
}

const over5Arr = [1,2,3,4,5,6,7,8,9,10].filter( over(5) );
// => over init!
console.log( over5Arr ); // => [ 6, 7, 8, 9, 10 ]

over() は1度だけ呼び出されて、その後はover(5) で返された関数が実行される。( filter のループ内で都度 over() が呼ばれる訳ではない)

高階関数でロジックを分離しているので、条件を変えた処理を短いコードで表現できるようになる!!

require('./Array-reject');

// data
const idols = [
  { name: '星宮 いちご',   type: 'cute' },
  { name: '霧矢 あおい',   type: 'cool' },
  { name: '紫吹 蘭',      type: 'sexy' },
  { name: '有栖川 おとめ', type: 'pop' },
  { name: '藤堂 ユリカ',  type: 'cool' },
  { name: '神谷 しおん',  type: 'cool' },
  { name: '一ノ瀬 かえで', type: 'pop' },
  { name: '三ノ輪 ヒカリ', type: 'sexy' },
  { name: '神崎 美月',    type: 'sexy' },
  { name: '夏樹 みくる',  type: 'pop' },
  { name: '北大路 さくら', type: 'cute' },
  { name: '大空 あかり',  type: 'cute' },
  { name: '服部 ユウ',   type: 'cool' },
  { name: '氷上 スミレ',  type: 'cool' },
  { name: '新条 ひなき',  type: 'pop' },
  { name: '紅林 珠璃',   type: 'sexy' },
  { name: '黒沢 凛',    type: 'cool' },
  { name: '天羽 まどか', type: 'cute' },
];

// currying
function idolType(type) {
  return function(idol) {
    return idol.type === type
  };
}

// type "cool" のリスト
const coolIdols    = idols.filter( idolType('cool') );
// type "cool" 以外のリスト
const notCoolIdols = idols.reject( idolType('cool') ); 
// type "cute" のリスト
const cuteIdols    = idols.filter( idolType('cute') );

console.log( coolIdols );
/* [ { name: '霧矢 あおい', type: 'cool' },
  { name: '藤堂 ユリカ', type: 'cool' },
  { name: '神谷 しおん', type: 'cool' },
  { name: '服部 ユウ', type: 'cool' },
  { name: '氷上 スミレ', type: 'cool' },
  { name: '黒沢 凛', type: 'cool' } ]
*/
console.log( notCoolIdols );
/* [ { name: '星宮 いちご', type: 'cute' },
  { name: '紫吹 蘭', type: 'sexy' },
  { name: '有栖川 おとめ', type: 'pop' },
  { name: '一ノ瀬 かえで', type: 'pop' },
  { name: '三ノ輪 ヒカリ', type: 'sexy' },
  { name: '神崎 美月', type: 'sexy' },
  { name: '夏樹 みくる', type: 'pop' },
  { name: '北大路 さくら', type: 'cute' },
  { name: '大空 あかり', type: 'cute' },
  { name: '新条 ひなき', type: 'pop' },
  { name: '紅林 珠璃', type: 'sexy' },
  { name: '天羽 まどか', type: 'cute' } ]
*/
console.log( cuteIdols );
/* [ { name: '星宮 いちご', type: 'cute' },
  { name: '北大路 さくら', type: 'cute' },
  { name: '大空 あかり', type: 'cute' },
  { name: '天羽 まどか', type: 'cute' } ]
*/

文章みたいな構造でリスト操作ができるようになりました!
A W E S O M E !

 
完 全
理 解


アイカツ!をみよう!!

RSpec Capybara 非表示な要素のテストにハマる

RSpec でテストをしていて、非表示にしてある要素が取れずにハマった。

Capybara さんはデフォルトでは画面上に見える要素のみを検索するので display: nonehidden になっている要素を触るできないっぽい。

<div class="modal" style="display:none"></div>
<input type="hidden" id="secret_val" value="XXX">
scenario 'モーダルが存在する' do
  expect(page).to have_selector('.modal')
end

scenario '#secret_val の値は XXX' do
  expect( find('#secret_val').value ).to eq 'XXX'
end

画面に見えてない要素は触れないのでテストが通らない

visible: false オプションを使う

scenario 'モーダルが存在する' do
  expect(page).to have_selector('.modal', visible: false)
end

scenario '#secret_val の値は XXX' do
  expect( find('#secret_val', visible: false).value ).to eq 'XXX'
end

👉 success
オプションを付ければ非表示要素も検索してくれるのでテストが可能になる!

Options Hash (**options):
visible (Boolean, Symbol) — Only find elements with the specified visibility:

  • true - only finds visible elements.
  • false - finds invisible and visible elements.
  • :all - same as false; finds visible and invisible elements.
  • :hidden - only finds invisible elements.
  • :visible - same as true; only finds visible elements.

cf. Module: Capybara::Node::Finders — Documentation for jnicklas/capybara (master)

非表示になっている要素のコンテンツは have_content / have_text で取れない

例えばデフォルトで非表示になっているモーダルのタイトルの内容があっているかテストしたいような場合

<div class="modal" style="display:none">
  <p class="modal_title">アイカツを見ろ</p>
</div>
scenario '正しいモーダルが出力されていること' do
  expect( find('.modal', visible: false) ).to have_content 'アイカツを見ろ'
end

👇

Failure/Error: expect( find('.modal', visible: false) ).to have_content 'アイカツを見ろ'
   expected to find text "アイカツを見ろ" in "". (However, it was found 1 time including non-visible text.)

非表示の要素は取れていても have_contenthave_text は 空 ("") と判断されてテストが落ちてしまう

have_selectortext: オプションも使ってマッチさせることでコンテンツ内容のテストができる

但し、非表示要素内の要素は have_selector でも visible オプションが必要

非表示なコンテナ内にあって、直接 display: none などのスタイルを当てられててない要素でも visible オプションがないと要素を取得できずにテストが落ちてしまう

scenario '正しいモーダルが出力されていること' do
  expect( find('.modal', visible: false) ).to have_selector('.modal_title', text: 'アイカツを見ろ')
end

👇

Failure/Error: expect( find('.modal', visible: false) ).to have_selector('.modal_title', text: 'アイカツを見ろ')
   expected to find visible css ".modal_title" with text "アイカツを見ろ" within #<Capybara::Node::Element tag="div" path="/HTML/BODY[1]/DIV[2]/DIV[2]/DIV[2]/DIV[1]"> but there were no matches. Also found "", which matched the selector but not all filters.

have_selector にも visible オプションを付ける

scenario '正しいモーダルが出力されていること' do
  expect( find('.modal', visible: false) ).to have_selector('.modal_title', visible: false, text: 'アイカツを見ろ')
end

👉 success
これならテストが通る!
ただ感覚的に「◯◯のテキスト が 'XXXX'」となってる方が直感的なわかり易さもテストの構造としても良い気がするので微妙な感じ…

.text(:all) で非表示になっているテキストノードを取得できる

#text(type = nil, normalize_ws: false) ⇒ String
Retrieve the text of the element. If ignore_hidden_elements is true, which it is by default, then this will return only text which is visible. The exact semantics of this may differ between drivers, but generally any text within elements with display:none is ignored. This behaviour can be overridden by passing :all to this method.
cf. Method: Capybara::Node::Element#text — Documentation for jnicklas/capybara (master)

text(:all) でテキストを取得したほうが「◯◯のテキスト が 'XXXX'」と書けるので非表示になっている要素の内容のテストには良さそう

<div class="modal" style="display:none">
  <p class="modal_title">アイカツを見ろ</p>
  <div class="modal_body">
    <strong>アイカツオンパレード始まったから絶対見て!!!!</strong>
  </div>
</div>
scenario '正しいモーダルが出力されていること' do
  # 文字列に含まれるをテストするので include を使う
  expect( find('.modal', visible: false).text(:all) ).to include 'アイカツを見ろ'
end

👉 success
テストが通りました ٩(ˊᗜˋ*)و

非表示の要素内のテキストは find()visible オプション・ text():all 引数の両方がないとダメ

text:all がないと空("")になるのでテストが通らない

scenario '正しいモーダルが出力されていること' do
  expect( find('.modal', visible: false).text ).to include 'アイカツを見ろ'
end

👇

Failure/Error: expect( find('.modal', visible: false).text ).to include 'アイカツを見ろ'
   expected "" to include "アイカツを見ろ"

find()visible オプションがないとそもそも要素を取得できないのでテキストも取得できない

scenario '正しいモーダルが出力されていること' do
  expect( find('.modal').text(:all) ).to include 'アイカツを見ろ'
end

👇

Failure/Error: expect( find('.modal').text(:all) ).to include 'アイカツを見ろ'
  Capybara::ElementNotFound:
    Unable to find visible css ".modal"
text の引数はこんな感じになっているそうです

見えないテキスト(display:none)が text メソッドの戻り値に含まれるかどうかについて
2.1 では、text メソッドの引数によって挙動を切り替えることもできる

find("#thing").text           # Capybara.ignore_hidden_elements によって挙動が変わる
find("#thing").text(:all)     # 見えないテキストも含む
find("#thing").text(:visible) # 見えるテキストだけ
cf. capybara 2.1 を学ぶ - おもしろwebサービス開発日記

感想

  • 非表示になっている要素には visible: false オプションを使う
  • 非表示になっている要素の内容をテストするには find(selector, visible: false).text(:all) を使う

のが良さそうです。
 
RSpec も Capybara も何も分からん… (docker-mac でのテスト走らせるのが遅くて辛いからどうにかなってほしぃ…


[参考]

カピバラさん PCクッション

カピバラさん PCクッション

RSpec Capybara href の無い a タグにハマる

RSpecの feature spec で href の無い a タグのテストをしようとしてハマったのでメモ

ボタン / リンクの存在

ボタン

buttonsubmit

<button>ボタンのラベル</button>
<input type="submit" value="ボタンのラベル" />
expect(page).to have_button 'ボタンのラベル'
リンク

a タグはボタンではなくリンクでないとマッチしない

<a href="example">リンクテキスト</a>
expect(page).to have_link 'リンクテキスト'

リンク先 (href) も含めてマッチ

expect(page).to have_link 'リンクテキスト', href: 'example'

ボタン / リンクのクリック

Capybara でクリックさせる

ボタン

buttonsubmit

<button>ボタンのラベル</button>
<input type="submit" value="ボタンのラベル" />
click_button 'ボタンのラベル'
リンク

a タグ

<a href="example">リンクテキスト</a>
click_link 'リンクテキスト'

ボタンまたはリンク

button, submit, 'a' タグ どれでもマッチする

<button>ボタンのラベル</button>
<input type="submit" value="ボタンのラベル" />
<a href="example">リンクテキスト</a>
click_on 'ボタンのラベル'
click_on 'リンクテキスト'

href が無い a タグは have_link, click_link, click_on にマッチしない

<a>リンクテキスト</a>
expect(page).to have_link 'リンクテキスト'

👇

Failure/Error: expect(page).to have_link 'リンクテキスト'
  expected to find link "リンクテキスト" but there were no matches

href が無い a タグはマッチせず存在しないと言われてしまいました…

a tags without an href are not links, they are placeholders for links. That's how the HTML spec defines it, that's how every modern browser treats them. Capybara does indeed only click on links which have the href attribute, and imho, that's sensible behaviour. click_link 'foo' not working on links without href · Issue #379 · teamcapybara/capybara · GitHub

href がないとリンクではないからと言うことのみたい

href のない a タグの存在

have_selector を使う

<a>リンクテキスト</a>
expect(page).to have_selector('a', text: '<リンクテキスト>')

href のない a タグをクリック

find で要素を選択してクリックさせる

<a>リンクテキスト</a>
find('a', text: 'リンクテキスト').click

a タグに CSS のクラス名があるならクラス名で find してもOK

<a class="link_icon">
  <i class="icon"></i>
</a>
find('.link_icon').click

 

RSpec 時々しか書かないから何も分からん…


[参考]