やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

ElixirのString.contains?で第1引数にbinaryを第2引数に日本語ひらがなを与えるとfalseになる理由についての調査報告

事の発端

たまたまElixirでhttp responseのbinary情報に対して、特定の日本語が含まれているかという判定式を記述していたところで、この現象に遭遇した。

iex> body = <<201, 202, 197, ...>>
iex> String.contains?(body, "いちご")
false

間違いなくbinaryの中には第2引数で渡している日本語ひらがな(内部データ的にはこいつもbinary)が含まれているのになぜかfalseになる。この一件をtwitterにて投下したところ、KIKUCHI Yutaka 🌓 菊池 豊さんとこの動作について議論をしたが、なぜそうなるのかの答えにはたどり着けなかった


以降、時間が上手く確保できずで調査をする時間がなかったのだが、空いた時間を作れたのでなぜそうなるのかを調べてみた

問題の判定

iex> ?a
97

iex> ?b
98

iex> ?c
99

iex> ?あ
12354

iex> ?い
12356

iex> ?う
12358

なぜfalseになるのか...

iex> String.contains?(<<97, 98, 99>>, "abc")
true

iex> String.contains?(<<12354, 12356, 12358>>, "あいう")
false

2byteずつ確保されているなら以下の処理はtrueになるのかどうか(ならない)

iex> String.contains?(<<12354, 12355, 12356, 12357, 12358, 12359>>, "あいう")
false

String.contains?の実装を見てみる

まずは公式ドキュメントから。この時点で解決出来るのがベスト

Checks if string contains any of the given contents.
contents can be either a string, a list of strings, or a compiled pattern.

文字列に与えられたコンテンツが含まれているかどうかを確認。 コンテンツは文字列、文字列リスト、もしくはコンパイルされたパターンのどれか。

なるほど、コードサンプルも確認しておこう。第2引数をリストにして複数からor検索で実行出来るのは知らなかった

String.contains?("elixir of life", "of")
true
String.contains?("elixir of life", ["life", "death"])
true

なにやら見慣れない使い方を発見。確かにcompile patternを引数に渡せると説明にあったけど、そもそもcompile patternが何か分からん

iex> pattern = :binary.compile_pattern(["life", "death"])
iex> String.contains?("elixir of life", pattern)
true

:binaryということはErlangのモジュールなので、Erlangのドキュメントを確認しに行く

about binary module

This module contains functions for manipulating byte-oriented binaries.
Although the majority of functions could be provided using bit-syntax,
the functions in this library are highly optimized and are expected to either execute faster or consume less memory,
or both, than a counterpart written in pure Erlang.

このモジュールには、バイト指向のバイナリを操作するための関数が含まれています。
ほとんどの関数はビット構文を使用して提供しており、このライブラリの関数は高度に最適化されており、
純粋なErlangで記述された関数よりも高速に実行される、もしくはメモリの消費量が少なくなります。

思った通り、バイナリ操作をするための関数群らしい。先ほど、登場した:binary.compile_patternについて確認する。 少々長いので、部分的に公式ドキュメントを引用する。

Builds an internal structure representing a compilation of a search pattern
When a list of binaries is specified, it denotes a set of alternative binaries to search for

なるほど。本当に関数名のまんまでErlangではmatch/3, matches/3などの関数で使用するための検索パターンをバイナリから作成するための関数のよう。 リストの場合も同様で、引数で渡すときはflatなデータを代入してくれなど諸注意についても記述されている。何となく意味と使われ方が分かったので次に進もう

ちなみに以下のコードも試したがダメだった

iex> pattern = :binary.compile_pattern(["", "", ""])
{:ac, #Reference<0.2806410432.2574385153.63561>}

iex> String.contains?(<<12354, 12356, 12358>>, pattern)   

内部実装を見に行こう

compile patternというものも試してみたがtrueに判定されなかった。そもそもcompile patternについての知識が乏しいというのもあるが、一旦考えないこととする。実際にString.contains?がどのように判定を行なっているかを確認するために、Stringモジュールを見てみる

https://github.com/elixir-lang/elixir/blob/v1.10.0/lib/elixir/lib/string.ex#L2188

def contains?(string, []) when is_binary(string) do
  false
end

def contains?(string, contents) when is_binary(string) and is_list(contents) do
  "" in contents or :binary.match(string, contents) != :nomatch
end

def contains?(string, contents) when is_binary(string) do
  "" == contents or :binary.match(string, contents) != :nomatch
end

まずcontains?に関してはパターンマッチを使用して3種類の関数が実装されている。 共通の条件としては第1引数がバイナリであること。ドキュメントに記述があるように、第1引数が空文字の場合にAll matchingtrueを返すようなのでor条件式。それに加えた以下の条件によって3つの関数を使い分けしているようだ

  • 1.第2引数が空のリストの場合に固定でfalseを返す
  • 2.第2引数がリストであり、要素を持っている。:binary.match(string, contentes) != :nomatchではない
  • 2.第2引数がリストではなく(排反的に):binary.match(string, contentes) != :nomatchではない

ここで、再びErlangのドキュメントの:binary.matchに戻る。おそらく、こちらでもパターンマッチを使用して複数の関数が定義されているだろう
内部で実装されているErlangbinary.match/2を直接呼び出してもfalseになる

iex> :binary.match(<<12354, 12356, 12358>>, "あいう")
:nomatch

binary.match/2 について

Erlang Official Document: binary.matchから引用

match(Subject, Pattern) -> Found | nomatch OTP R14B
Types
  Subject = binary()
  Pattern = binary() | [binary()] | cp() # 第2引数がbinaryかbinaryを要素に持つリスト、もしくはcompile pattern
  Found = part()
Same as match(Subject, Pattern, []).

https://github.com/elixir-lang/elixir/blob/v1.10.0/lib/elixir/lib/string.ex#L2188

-spec match(Subject, Pattern) -> Found | nomatch when
      Subject :: binary(),
      Pattern :: binary() | [binary()] | cp(),
      Found :: part().

match(_, _) ->
    erlang:nif_error(undef).

え、どういうこと。これで関数として判定が成り立つってこと?? 単純にErlangsyntaxを理解出来ていないのか。
returnとしてFound :: part()もしくはnomatch(atom)を返すのは分かるけど、判定をどこでしてるのかが全く分からない。判定をどうやっているかが分からないと今回一番見たい部分を見る事が出来ない。

とりあえず正常にmatchした時のresponseを落ち着いて1回、見てみる

iex> :binary.match("あいう", "あいう")
{0, 9}

このreponseが先ほど確認したFound :: part()に当たるものだろう。part()は内部でbinary()を返している:

-spec part(Subject, PosLen) -> binary() when
      Subject :: binary(),
      PosLen :: part().

気を取り直して、binaryのドキュメントをよく見てると判定をどのようにしているかの旨が記述されているではないか。

part() = {Start :: integer() >= 0, Length :: integer()}
A representaion of a part (or range) in a binary. Start is a zero-based offset into a binary() and Length is the length of that part.
As input to functions in this module, a reverse part specification is allowed,
constructed with a negative Length, so that the part of the binary begins at Start + Length and is -Length long.
This is useful for referencing the last N bytes of a binary as {size(Binary), -N}. The functions in this module always return part()s with positive Length.

先ほど確認したresponseは{Start :: integer() >= 0, Length :: integer()}は上記のように構成されており、Start()binary()という基準点からの距離?(offset...うーん、いまいち何を言ってるのか分からないが)を持つらしい。
こんな時は頭を空っぽにして、コードの実行結果を見てみよう。Erlangのドキュメントを参考にpart()関数を呼び出してみる

iex> bin = <<1,2,3,4,5,6,7,8,9,10>> 
<<1, 2, 3, 4, 5, 6, 7, 8, 9, 10>>

iex> :binary.part(bin, {byte_size(bin), -5})        
<<6, 7, 8, 9, 10>>

あーなるほど、offsetと言っているのはsliceを行うというような意味合いなのか。とすると内部でやっていることは大したことではないはず。今回はpart()の第2引数を-5で固定で渡したけど、Elixircontains?からは何が渡っているのだろうか...(後日談: これあんまり関係なかった)
先ほどの戻り値は0番目の位置から9byte進んだところまで一致したということを表しているのか

iex> :binary.match("あいう", "あいう")
{0, 9}

つまり、「あ、い、う」それぞれが3byteずつ容量を持っているということか? そう思い<<12354, 12356, 12358>>をiexに打ち込んでみたところ、「あいう」に変換されないことに気づく

iex> bin = <<12354, 12356, 12358>>
"BDF"

iex> :binary.part(bin, {byte_size(bin), -2})
"DF"

どういうことだ..?? byteの情報とcodepointsの情報が一致しないものがあるということだとすれば辻褄が合うが...

# やはり以下の番号が返ってくる
iex(33)> String.to_charlist("あいう")
[12354, 12356, 12358]

そもそもErlangにはcodepointsという概念がないのかもしれないと思い、binarybinary listに変換するbinary.bin_tolistを試してみたところ、とんでもないことが分かった。やはり先ほどの予測通り、日本語ひらがなは3byteの情報を持っているようだ

# よく見ると配列の一番最後の要素の値が2ずつインクリメントされている
iex> :binary.bin_to_list("")
[227, 129, 130]
iex> :binary.bin_to_list("")
[227, 129, 132]
iex> :binary.bin_to_list("")
[227, 129, 134]

仮説が正しいのかを確認

iex> bin = <<227, 129, 130, 227, 129, 132, 227, 129, 134>>
"あいう"

素晴らしい。これならtrueの判定を見る事が出来そうだ

iex> String.contains?(<<227, 129, 130, 227, 129, 132, 227, 129, 134>>, "あいう")
true

なるほど、やはりそうだった。内部でcallしているのがErlangのモジュールに実装された関数であるため、byte情報の取り扱い方が異なるのが原因だと考えられる。Erlangでは日本語ひらがな1文字は3byteの情報で扱うのだが、Elixirでは1byteの情報として扱っている。この違いのせいで、思ったようにtrueの判定にならなかったのだろう

残る疑念

Elixirではbinary情報を1byteで扱っている、Erlangでは日本語ひらがなに関しては3byteで扱っていると記述しているが、これは本当にbyteなのか。単位が正確ではない気がする。1codepointが正しい??

参考文献

【擬似実装コード有り】Pythonでswitch構文っぽいものを表現する方法について

かゆい所に手が届くswitch構文

別にswitch構文がなくても、おそらくその処理は工夫次第で記述することが可能だろう。しかし、golangjavascriptに見られるswitch構文を用いることで少なくとも可読性は上がるし、拡張性もif elseの組み合わせよりは良いはず。それにif elseの組み合わせで複雑な多条件を表現すると階層化されて条件がネストされるため視認性が下がるので、好きではない。switch構文なら見かけ上ではcaseA, caseB, caseC...は対等に見えるので視認性はこちらの方が良い(実際の内部処理は上から辿っている模様)

別にswitchがなくても処理は書ける
golang

func FizzBuzz(num int) string {
  if num % 15 == 0 {
    return "fizzbuzz"
  } else if num % 3 == 0 {
    return "fizz"
  } else if num % 5 == 0 {
    return "buzz"
  }
  return "no match"
}

switch構文を使えば可読性と拡張性が増す
golang

func FizzBuzz(num int) string {
  // switchに渡す条件をtrueにしておけば条件式を記述出来る
  switch true {
    case num % 15 == 0:
      return "fizzbuzz"
    case num % 3 == 0:
      return "fizz"
    case num % 5 == 0:
      return "buzz"
  }
  return "no match"
}

仮に「七の倍数の時はラッキーって出力しておいて〜」という仕様が急に決まったとしても以下のようにするだけで改修は終わる(ただし、評価順は上から順なので注意)

// 7の倍数という条件の方を優位にさせるため、 num % 3 == 0の上に記述(最初に21で被る)
func FizzBuzz(num int) string {
  // switchに渡す条件をtrueにしておけば条件式を記述出来る
  switch true {
    case num % 15 == 0:
      return "fizzbuzz"
    case num % 7 == 0:
      return "ラッキー"
    case num % 3 == 0:
      return "fizz"
    case num % 5 == 0:
      return "buzz"
  }
  return "no match"
}

「お、こんな便利な構文使わん理由ないやん、よっしゃPythonでも書いたろ」と思っても残念。Pythonにはswitch構文は実装されていない。理由は以下の公式ドキュメントのQAにて書かれている通り

docs.python.org

if... elif... elif... else の繰り返しで簡単に同じことができます。switch 文の構文に関する提案がいくつかありましたが、範囲判定をするべきか、あるいはどのようにするべきかについての合意は (まだ) 得られていません。

一言で言えば、他に書く方法あるから、それで何とかしてね。って感じかな。おっしゃる通りだけど、あってもいい気はする。で、このPythonswitch構文を導入するかどうかは2001年頃から議論されており、拒否されているよう。
www.python.org

それでもそれっぽいswitch構文が使いたい

です。なので、それっぽいのを記述する。すでに多くの先駆者がif else使ったり、dictionaryにkeyをセットして関数をvalueに持たせたりと既出のものが多いので自分が調べた限り、この書き方は確認出来なかったので載せておく。正直な所はO(1)で高速にアクセス可能なdictionaryのkeyとvalueを使った複数条件処理を推したいところではある

if false: でネストさせるやつ

どういうこと。とりあえずコードを見せる

def fizzbuzz(num):
  """
    num -> int
    return -> int
  """
  if False:
    pass
  elif num % 15 == 0:
    print("fizzbuzz")
  elif num % 3 == 0:
    print("fizz")
  elif num % 5 == 0:
    print("buzz")

なぜif False:と一番上の条件式に記述をしているかというと、先程話した通り、条件式が見た目上、ネストするのを回避するためと後の拡張性を確保したいからだ。処理としては一判定無駄になってしまうが、前者の理由を優先した。あとはswitch構文同様にcaseと記述して条件式を書くもの、ifswitch構文っぽく記述するなら全てelifという様に記述できるように意識した。正直、好みの問題だし下記の記述がdefaultだろう

def fizzbuzz(num):
  """
    num -> int
    return -> None
  """
  if num % 15 == 0:
    print(num, " is fizzbuzz")
  elif num % 3 == 0:
    print(num, " is fizz")
  elif num % 5 == 0:
    print(num, " is buzz")

無名関数のリストを作るやつ

無名関数を使ってこんなことが出来る

lst = [lambda x: x > 5, lambda x: x < 3]

for i, func in enumerate(lst):
  print(i, "->", func(i))

実行結果

0 -> False # 0 > 5
1 -> True # 1 < 3

この様に無名関数をリストに保持させておくことで実行順序を保証できて、条件式を実行時評価状態にしておくことが出来るので、複雑な条件式を表現可能ということでswitch構文っぽいものが記述出来る。dictionaryを使う場合にはkeyをa, b, cのようにsortされても問題ないように順序を意識してkeyを作成する必要があるため、うーんとなり、この方法が思いついた

# 条件式を内包する無名関数を作成(実行時に評価される)
# Falseを返しているのは実行時に評価式がFalse判定されたことを呼び出し元に伝えるため
conditions = [
  lambda x: "fizzbuzz" if x % 15 == 0 else False,
  lambda x: "fizz" if x % 3 == 0 else False,
  lambda x: "buzz" if x % 5 == 0 else False
]

# 擬似switch構文(値と条件式を含む無名関数のリストを受け取る)
def switch(val, judge_lst, default_res=None):
  """
    # val -> any
    # judge_lst -> list[func]
    # default_res(None) -> any
    # return -> any
  """
  # リストに含まれている関数を順に実行(リストなので)
  for func in judge_lst:
    res = func(val)
    # Flaseが返ってきていない(評価式がtrueとなった)ならbreak
    # この処理を削除すればフォールスルーになる(breakをしないと次の評価式に移るやつ)
    if res:
      return res
  return default_res

実行結果

for i in range(1, 31):
  print(i, " -> ", switch(i, conditions, "no match"))

# 1  ->  no match
# 2  ->  no match
# 3  ->  fizz
# 4  ->  no match
# 5  ->  buzz
# 6  ->  fizz
# 7  ->  no match
# 8  ->  no match
# 9  ->  fizz
# 10  ->  buzz
# 11  ->  no match
# 12  ->  fizz
# 13  ->  no match
# 14  ->  no match
# 15  ->  fizzbuzz
# 16  ->  no match
#  :
# 30  ->  fizzbuzz

今回はサンプルのために、ただ文字列を返すという無名関数を作成したが、無名関数内から定義済み関数をcallしたり、クラスのinstanceを返したりと割とやれることは多いはず。ただ、if else...の実装に比べると明らかに重いので、上手く共通化出来る時ぐらいしか出番はないだろう

conditions = [
  lambda x: str_slicer("fizzbuzz") if x % 15 == 0 else False,
  lambda x: str_slicer("fizz") if x % 3 == 0 else False,
  lambda x: str_slicer("buzz") if x % 5 == 0 else False
]

def str_slicer(str_):
  """
    str_ -> string
    return -> list[string]
  """
  res = list()
  append = res.append
  for s in str_:
    append(s)
  return res

実行結果

for i in range(1, 31):
  print(i, " -> ", switch(i, conditions, "no match"))

# 1  ->  no match
# 2  ->  no match
# 3  ->  ['f', 'i', 'z', 'z']
# 4  ->  no match
# 5  ->  ['b', 'u', 'z', 'z']
# 6  ->  ['f', 'i', 'z', 'z']
# 7  ->  no match
# :
# 29  ->  no match
# 30  ->  ['f', 'i', 'z', 'z', 'b', 'u', 'z', 'z']

総評

すでにPythonswitch構文の代役に関する記事はそこそこあるが、個人的な興味と久しぶりにPythonを記述機会があったので遊んでみた。if else...を淡々と書き続けていると頭がおかしくなりそうなので、最近は無名関数に条件式埋め込んでってことをよくやる。記事の主題とは関係ないが、Pythonswitch構文がない理由を調べる中で、「デザインと歴史」というページにたどり着いて、少し読んでみたが面白かった。興味のある項目があったら読んでみるのも良いかもしれない

参考文献

【golang/Elixirのサンプル有り】何度も同じようなテストを書くのがつらいので無名関数を使って楽しくしよう

反応が良かったツイート

テストを無名関数で書くと楽しいよという旨のツイートの反応が良かった。ただ言葉だけだと正確に情報が伝わらないので実際にどうやっているのかをコードに落としてみた

せっかくなのでgolangElixirで書いてみた

実際に無名関数を使ってテストを書いてみる

リストに含まれている値を集計し、登場回数をカウントしてmapにして返す

  • go version: go1.12.4 darwin/amd64
  • Elixir version: Erlang/OTP 22 & Interactive Elixir (1.9.4)

golang

動作の確認

func CountAggregeter(lst []string) map[string]int {
  // 集計用のmap
  aggregater := make(map[string]int, 0)
  for _, val := range lst {
    if _, ok := aggregater[val]; ok {
      aggregater[val] += 1
    } else {
      aggregater[val] = 1
    }
  }
  return aggregater
}
// 実行結果
package main
import "fmt"
func main(){
    // Your code here!
    
    lst := []string{"AAA", "BBB", "AAA", "CCC", "DDD", "BBB", "EEE"}
    res := CountAggregeter(lst)
    fmt.Println(res)
}

// map[AAA:2 BBB:2 CCC:1 DDD:1 EEE:1]

こちらからも確認可能です
play.golang.org

プロジェクト化

$GOPATH以下にディレクトリを作成してファイルを用意する(今回はテストだけを試すのでmain.goはなし)。テストのパッケージはgolangに標準組み込みのtestingを使用する。業務で3ヶ月使っているがシンプルなテストをする上でパフォーマンスは十分で覚えることもほとんどない。

ちなみに$GOPATHは簡単に確認することが出来る

$ echo $GOPATH
/Users/okb/go

/nameless_func_test  
|── /utils  
    |── list.go  
    |── list_test.go  

下記に先ほど作成したリストの値をカウントする関数を記述する
./nameless_func_test/utils/list.go

package utils

// リストを受け取り、値の登場回数をカウント
func CountAggregeter(lst []string) map[string]int {
    // 集計用のmap
    aggregater := make(map[string]int, 0)
    for _, val := range lst {
        // 既出であれば+1
        if _, ok := aggregater[val]; ok {
            aggregater[val] += 1
        } else {
            // 初登場であれば1を格納
            aggregater[val] = 1
        }
    }
    return aggregater
}

testingの仕様に従って、_test.goをテストを行いたいファイルの語尾に付ける。今回はlist.goに対してテストを行うのでlist_test.goを作成。そしてお待たせ。ここで無名関数が登場する
./nameless_func_test/utils/list_test.go

// Testという名前をテストを行いたい関数の頭に付けて関数を作成
func TestCountAggregeter(t *testing.T) {
        // 何度も同じ処理を行うため無名関数化
    debuger := func(lst []string, expectMap map[string]int) error {
        // 集計処理を実行
        res := CountAggregeter(lst)

        // 期待する値を用いてloop処理
        for key, val := range expectMap {
            // 値が集計結果に含まれているかどうか
            if cnt, ok := res[key]; ok {
                // カウント数が一致しないのであればerror
                if val != cnt {
                    return errors.New("[Error] カウント数が一致しません")
                }
            } else {
                // そもそも結果に含まれていなければerror
                return errors.New("[Error] 想定値か集計値が間違っています")
            }
        }
        return nil
    }
}

これがテストに使用する無名関数。引数にCountAggregeterに渡すリストと、期待する結果(assertテストとほとんど同じ)を与えて、一致するかをrangeを使って確認するというシンプルな仕上がり。退屈なテストの記述作業が少しは面白くなるはず。
あとはこの無名関数を使ってテストをゴリゴリ書くだけ(ちなみにtestingではエラー終了をさせる時にt.Error()とする)

./nameless_func_test/utils/list_test.go

func TestCountAggregeter(t *testing.T) {
    // シンプルなケース
    if err := debuger([]string{"A", "B", "C", "D"}, map[string]int{"A": 1, "B": 1, "C": 1, "D": 1}); err != nil {
        t.Error(err)
    }

    // 重複した値がうまくカウントされているか
    if err := debuger([]string{"A", "B", "A", "C"}, map[string]int{"A": 2, "B": 1, "C": 1}); err != nil {
        t.Error(err)
    }

    // リストが空の場合
    if err := debuger([]string{}, map[string]int{}); err != nil {
        t.Error(err)
    }

    // 複数の値が重複する場合
    if err := debuger(
        []string{"A", "B", "A", "C", "D", "C", "E", "F", "D", "F", "G"},
        map[string]int{"A": 2, "B": 1, "C": 2, "D": 2, "E": 1, "F": 2, "G": 1},
    ); err != nil {
        t.Error(err)
    }

    // 重複の発生が3回以上
    if err := debuger([]string{"A", "A", "A"}, map[string]int{"A": 3}); err != nil {
        t.Error(err)
    }
}

テストの実行と結果の確認。いいね

$ pwd
/Users/okb/go/src/nameless_func_test

$ go test -v nameless_func_test/utils
=== RUN TestCountAggregeter
--- PASS: TestCountAggregeter (0.00s)
PASS
ok nameless_func_test/utils 0.007s

Elixir編

実はElixirでテストを扱うのは初めて。今まではやってもdoctestでしか記述をしたことが無かった。まずはCountAggregeterと同様の機能を持つ関数の実装を行なった

defmodule Aggregater do
  def counter(lst) do
    Enum.reduce(lst, %{}, fn val, acc ->
      if Map.has_key?(acc, val) do
        Map.put(acc, val, Map.get(acc, val)+1)
      else
        Map.put(acc, val, 1)
      end
    end)
  end
end

Aggregater.counter(["AAA", "BBB", "AAA", "CCC", "DDD", "BBB", "EEE"]) |> IO.inspect

実行結果

> $ iex list.ex  
Erlang/OTP 22 [erts-10.6.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]

%{"AAA" => 2, "BBB" => 2, "CCC" => 1, "DDD" => 1, "EEE" => 1}
Interactive Elixir (1.9.4) - press Ctrl+C to exit (type h() ENTER for help)

プロジェクト化 & テスト書く

Elixirでのテストの記述に関してはmoduleレベルで扱うのは初めてなので以下の記事を参考にテストを書いてみる

elixirschool.com

ExUnitを扱いたいのでmixの新規プロジェクトを立ち上げる

$ mix new aggregater

実行後色々と生成される。今回、扱うのはlib/aggregater.extest/aggregater_test.exの2ファイルのみ。テストとして実行するファイルには.exs形式を用いる必要がある模様。

まずはlib/aggregater.exに先ほど実装した関数を記述
./lib/aggregater.ex

defmodule Aggregater do
  def counter(lst) do
    Enum.reduce(lst, %{}, fn val, acc ->
      if Map.has_key?(acc, val) do
        Map.put(acc, val, Map.get(acc, val)+1)
      else
        Map.put(acc, val, 1)
      end
    end)
  end
end

続いてテストのための無名関数を記述していく
./test/aggregater.ex

defmodule AggregaterTest do
  use ExUnit.Case
  doctest Aggregater

  # テストの命名
  test "test for Aggregater.counter" do
    # lst -> []string
    # expects -> map[string]int
    # 無名関数の作成
    debuger = fn lst, expects ->
      res = Aggregater.counter(lst)
      Enum.each(expects, fn {key, val} ->
        if Map.has_key?(res, key) do
          if val != Map.get(res, key) do
            IO.puts("#{val} == #{Map.get(res, key)}")
            # 想定外の状態が発生した場合に即return
            raise :error
          else
            :ok
          end
        else
          raise :error
        end
      end)
    end
  end
end

無名関数を使って複数のテストを書いてみる

defmodule AggregaterTest do
  use ExUnit.Case
  doctest Aggregater

  test "test for Aggregater.counter" do
    # シンプルなケース
    assert debuger.(["A", "B", "C", "D"], %{"A" => 1, "B" => 1, "C" => 1, "D" => 1}) == :ok

    # 重複した値がうまくカウントされているか
    assert debuger.(["A", "B", "A", "C"], %{"A" => 2, "B" => 1, "C" => 1}) == :ok

    # リストが空の場合
    assert debuger.([], %{}) == :ok

    # 複数の値が重複する場合
    assert debuger.(["A", "B", "A", "C", "D", "C", "E", "F", "D", "F", "G"], %{"A" => 2, "B" => 1, "C" => 2, "D" => 2, "E" => 1, "F" => 2, "G" => 1}) == :ok

    # 重複の発生が3回以上
    assert debuger.(["A", "A", "A"], %{"A" => 3}) == :ok
  end
end

実行してみる

$ mix test Finished in 0.06 seconds
1 test, 0 failures

Elixirで無事に無名関数を使ってテストを実行することが出来た。mixで作成したプロジェクトであればゴリゴリとテストが書いていけそうだ。人によるが自分にとっては退屈なテストケースの記述処理が無名関数を使うことで楽しく効率化することが出来る。ご参考に。全体のコードはこちらから
github.com

追記

いつもお世話になっているfukuoka.exさんの古賀さんより、一筆頂きました

サンプルコードまで書いて頂きました。確かにElixirでテストを書くのであればブロックを分けてシンプルなassertを一文記述する方式の方がどこでErrorになるのかも一目瞭然でクリーンになる。さらにPowerAssertというライブラリがかなりいい感じですね gist.github.com

PowerAssert

t.co

こういう反応がもらえるのが何よりありがたいし、自分のレベルアップが出来る。ありがとうございます🙇‍♂️

参考文献

数値をASCIIを用いてaからzまで(半角英字)の文字列に変換

何をしたいのか

業務で書いたコードなのだが、作成する元になったアイディアがボツになったため、お蔵入り。需要は無いだろうけど、せっかくなので当時、ググっても出てこなかったので公開しておこうと思う。1から始まる任意の数字をASCIIで定義されている数値と文字の紐付け表からaからzの適切な小文字アルファベット(半角英字)に変換するという処理だ。forで生成されるiterationのカウントを使ってa-zの文字列を生成するために使用していた。言葉で伝えても分かりづらいので動作の例を下記に記す

www.asciitable.com

i = 1 -> a
i = 2 -> b
:
i = 26 -> z
i = 27 -> a
i = 28 -> b
:
:

業務ではgolangを使って記述したのだが、せっかくなのでElixirで書き直した

予備知識

アルファベットとはご存知の通り、a-zで合計26文字が定義されている。またASCIIでは小文字のa97、小文字のz122として定義している。つまりは、26回周期でまた同じ半角英字が現れるということになる

アルファベット番号 ASCII番号
a 97
b 98
z 122

実際のコード

golang

// 数値をasillに変換する関数(eg: 97 -> a, 122 -> z)
func IntToASCII(num int) int {
    // 26 -> アルファベットの数
    if num > 26 {
        div := num / 26
        // 割り切れる際に整合性を取るため-1
        if num%26 == 0 {
            div -= 1
        }
        num -= 26 * div
    }
    // aが97で始まるため+97
    return num + 96
}

実行結果

package main

import (
    "fmt"
)

func main() {
    for i := 1; i < 100; i++ {
        res := IntToASCII(i)
        fmt.Println("i = ", i, ", res = ", res, ", str = ", string(res))
    }
}
i =  1 , res =  97 , str =  a
i =  2 , res =  98 , str =  b
i =  3 , res =  99 , str =  c
:
i =  24 , res =  120 , str =  x
i =  25 , res =  121 , str =  y
i =  26 , res =  122 , str =  z
i =  27 , res =  97 , str =  a
i =  28 , res =  98 , str =  b
:
i =  51 , res =  121 , str =  y
i =  52 , res =  122 , str =  z
i =  53 , res =  97 , str =  a
i =  54 , res =  98 , str =  b
:
i =  95 , res =  113 , str =  q
i =  96 , res =  114 , str =  r
i =  97 , res =  115 , str =  s
i =  98 , res =  116 , str =  t
i =  99 , res =  117 , str =  u

全文のコード詳細な結果はこちら
play.golang.org

Elixir

ゴリ押し感はいなめない。こういう計算処理を関数型でゴリゴリ書こうとするとどうなるんだろうと考えさせられた

# 無名関数を定義
int_to_ASCII = fn num -> 
  if num > 26 do
    num - (div(num, 26) + if(rem(num, 26)==0, do: -1, else: 0)) * 26 + 96
  else
    num + 96
  end
end

実行結果

Enum.each(1..100, fn n -> 
  res = int_to_ASCII.(n)
  IO.puts("i = #{n}, res = #{res}, str = #{[res]}")
end)
i = 1, res = 97, str = a
i = 2, res = 98, str = b
i = 3, res = 99, str = c
i = 4, res = 100, str = d
:
:
i = 97, res = 115, str = s
i = 98, res = 116, str = t
i = 99, res = 117, str = u
i = 100, res = 118, str = v

無名関数を使わないver
こっちの方がElixirっぽいかな

defmodule ASCII do
  def from_int(num) when num > 26 do
    num - calc_helper(num) * 26 + 96
  end
  def from_int(num), do: num + 96
  def calc_helper(num) do
    if rem(num, 26) == 0 do
      div(num, 26) -1
    else
      div(num, 26)
    end
  end
end

おまけ

Elixirのコードを後日さらにコンパクトに改良

defmodule ASCII do
  def from_int(num) when num > 26 do
    num - calc_helper(num) * 26 + 96
  end
  def from_int(num), do: num + 96
  def calc_helper(num) when rem(num, 26) == 0, do: div(num, 26) -1
  def calc_helper(num), do: div(num, 26)
end

参考文献

【サンプルコード有り】golangで複数条件のソートを無名関数を使っていい感じに実装してみた

何をしようとしているのか

struct(以降、構造体と表記)を要素に持つ、配列をソートする必要がある場面に出くわした。通常というか一般的な数値や文字列のソートと異なり、構造体のAフィールドの値が大きい順番かつ、Bフィールドの値が小さい順かつ...のようなソートを行う必要があるため、結構メンドくさい

一応、調べているとgolangにはパッケージ(組み込みではない)としてsortというものが用意されている。なお、sortの詳しい話は今回扱わないし、すでに素晴らしい情報が数多く公開されているのでそちらを参照して頂きたい。(参考文献にまとめています)

そんな中、こちらのブログで実装されている、構造体の複数fieldでのソート実装を見て、「なるほど、こんな感じでやればええんやな」と思い、ほとんどコピペで類似の処理を実装したものの、ソートする条件を関数化出来ない(ソートしたい各所でソート条件を持つ関数群を定義しなければならない)煩わしさに何とかならないかと思い試行錯誤した

or3.hatenablog.com

以下は引用のコードになります。非常に参考になりましたm( )m

func main(){
    // lessfuncを実装
    byBast := func(p1, p2 *profile)bool{
        return p1.threesize[0]<p2.threesize[0]
    }
    byClass := func(p1, p2 *profile)bool{
        return p1.class<p2.class
    }
    // 逆順も同じようにここで実装する
    byBastDescending := func(p1, p2 *profile)bool{
        return p1.threesize[0]>p2.threesize[0]
    }
    // 誕生日 time.TimeもBefore/Afterで比較してboolを返せる
    byDate := func(p1, p2 *profile)bool{
        return p1.data.Before(p2.data)
    }

    fmt.Println("学年昇順でおっぱい降順")
    sort.Sort(idleSorter{idle: aqours, lessfunc: []lessFunc{byClass, byBastDescending}})
    for i, v := range aqours{
        fmt.Printf("[%d] name: %-7s class: %d bast: %d\n", i+1, v.name, v.class, v.threesize[0])
    }
}

完成物としては条件となる文字列なりを渡してsplitし、lessFuncの配列に該当するソート条件を持つ無名関数を返すという処理の実装になる

出来たやつ

このブログ投稿を仮定した構造体を対象にソートを実行する無名関数を作成した

type Post struct {
    // データの作成者
    Name string
    // 投稿タイトル
    Title string
    // 投稿本文
    Body string
    // 作成日
    CreatedAt time.Time
}

ここまでは参考文献の実装と全く同じで、対象の構造体を変えただけ

// for sort
type lessFunc func(i, j *Post) bool
type PostSorter struct {
    Post []*Post
    lessFunc  []lessFunc
}

func (is PostSorter) Len() int {
    return len(is.Post)
}
func (is PostSorter) Swap(i, j int) {
    is.Post[i], is.Post[j] = is.Post[j], is.Post[i]
}
func (is PostSorter) Less(i, j int) bool {
    k := 0
    p, q := is.Post[i], is.Post[j]
    for k = 0; k < len(is.lessFunc)-1; k++ {
        less := is.lessFunc[k]
        switch {
        case less(p, q):
            return true
        case less(q, p):
            return false
        }
    }
    return is.lessFunc[k](p, q)
}

ここからがメイン。この関数で受け取った文字列からヒットする対象のソートを行うための無名関数を返す。なぜこのような形式になっているかというと、元々の実装がhttp経由でのGETquery stringを用いてsortすることを考えていたからだ

// 受け取ったstringからソートを行うための無名関数を返す
func SortRelationConverter(query string) func(*Post, *Post) bool {
    switch query {
    // Nameの昇順
    case "name":
        return func(p1, p2 *Post) bool {
            return p1.Name > p2.Name
        }
    // Nameの降順
    case "-name":
        return func(p1, p2 *Post) bool {
            return p1.Name < p2.Name
        }
    // Titleの昇順
    case "title":
        return func(p1, p2 *Post) bool {
            return p1.Title > p2.Title
        }
    // Titleの降順
    case "-title":
        return func(p1, p2 *Post) bool {
            return p1.Title < p2.Title
        }
    // Bodyの昇順
    case "body":
        return func(p1, p2 *Post) bool {
            return p1.Body > p2.Body
        }
    // Bodyの降順
    case "-body":
        return func(p1, p2 *Post) bool {
            return p1.Body < p2.Body
        }
    // CreatedAtの昇順
    case "created_at":
        return func(p1, p2 *Post) bool {
            return p1.CreatedAt > p2.CreatedAt
        }
    // CreatedAtの降順
    case "-created_at":
        return func(p1, p2 *Post) bool {
            return p1.CreatedAt < p2.CreatedAt
        }
    }
    return nil
}

最後にソート条件を渡しただけでソートの実行が出来るように上記の処理をラップする関数を用意。この関数にquery stringで受け取ったような形式で文字列を渡すだけでソートが実行される

// ソートを実行するためのラップ関数
// sortStr -> ソート条件(eg: name:-body:-created_at)
// splitSymbol -> ソート条件で区切り記号に用いる記号(eg: ':')
func PostSort(data []*Post, sortsStr, splitSymbol string) []*Post {
    // ソートを行うための無名関数を格納する配列
    sortCond := make([]lessFunc, 0)
    // ソート条件を対象の記号をsplitして作成
    sorts := strings.Split(sortsStr, splitSymbol)
    if len(sorts) > 0 {
        for _, sort := range sorts {
            // 条件から無名関数を作成して配列に格納
            sortFunc := SortRelationConverter(sort)
            sortCond = append(sortCond, sortFunc)
        }

        // ソートのために構造体を作成
        cond := PostSorter{
            Post: data,
            lessFunc:  sortCond,
        }
        // ソートを実行
        sort.Sort(cond)
    }
    return data
}

実行結果

func main(){
    // 検証用のデータを作成
    lst := []*Post{
        &Post{
            Name: "a",
            Title: "A-title",
            Body: "A-body",
        },
        &Post{
            Name: "b",
            Title: "B-title",
            Body: "B-body",
        },
        &Post{
            Name: "c",
            Title: "C-title",
            Body: "C-body",
        },
        &Post{
            Name: "d",
            Title: "D-title",
            Body: "D-body",
        },
        &Post{
            Name: "e",
            Title: "E-title",
            Body: "E-body",
        },
    }
    
    // 結果を確認するための無名関数
    debuger := func(lst []*Post) {
        fmt.Println("[debug] result")
        for _, val := range lst {
            fmt.Println("Name:", val.Name)
            fmt.Println("Title:", val.Title)
            fmt.Println("Body:", val.Body)
        }
        fmt.Println("------------------")
    }
    debuger(PostSort(lst, "name", ":"))
    debuger(PostSort(lst, "title:name", ":"))
    debuger(PostSort(lst, "name:-title", ":"))
}
[debug] result
Name: e
Title: E-title
Body: E-body
Name: d
Title: D-title
Body: D-body
Name: c
Title: C-title
Body: C-body
Name: b
Title: B-title
Body: B-body
Name: a
Title: A-title
Body: A-body
------------------
[debug] result
Name: e
Title: E-title
Body: E-body
Name: d
Title: D-title
Body: D-body
Name: c
Title: C-title
Body: C-body
Name: b
Title: B-title
Body: B-body
Name: a
Title: A-title
Body: A-body
------------------
[debug] result
Name: e
Title: E-title
Body: E-body
Name: d
Title: D-title
Body: D-body
Name: c
Title: C-title
Body: C-body
Name: b
Title: B-title
Body: B-body
Name: a
Title: A-title
Body: A-body
------------------

こちらから実際に結果を確認できます
play.golang.org

後日談

どうやら現在はもっと簡単に構造体を要素に持つ配列をソートできる模様。もっと簡単になりそう
mattn.kaoriya.net

参考文献