Edited at

Elixirらしい記述がわからない

More than 3 years have passed since last update.

ここ最近Elixirの勉強を初めて, ネット上のコードを見てきたりもしているのですが

いざ自分で簡単なコードを書いてみると, どんなコードもいまいち他の言語っぽかったり

逆に, 無理やりElixirらしくしているような違和感をおぼえたりしています。

ですので, ここで実際に「文字列を受け取り, 11文字目以降全体を「...」に変換した文字列を返す」という関数を, 3通りの方法で書いてみたいと思います.

どの書き方が良い、あるいはもっと別のよい書き方がなどの意見待ってます.


ALGOL族の伝統的記法

  @doc """                                                                                                                                                                                       

引数の文字列が10文字を超えていた場合、それを切り詰める
"""
def truncate(content) do
if String.length(content) > 10 do
String.slice(content, 0..9) <> "..."
else
content
end
end

一番最初に書いたのがこれ.

わかりやすい反面, いかにもらしくない記法.

いくらElixirがErlangの上にRuby風の構文をかぶせた言語だと言ってもこれはちょっと…という感想.


パターンマッチングを使った記法

  # 引数が文字列の場合                                                                                                                                                                           

# 文字リストに変換して下で定義された関数を呼び出す
def truncate(content) when is_binary(content) do
truncate( String.graphemes(content))
end

# 引数が文字リストで長さが10を超える場合
def truncate(content) when is_list(content) and length(content) > 10 do
content |> Enum.slice(0, 10) |> Enum.concat(["..."]) |> Enum.join
end

# 引数が文字リストの場合
# 長さは指定されていないが, 先に定義された関数との兼ね合いから自動的に長さ10以下の場合のみ呼び出されるはず
def truncate(content) when is_list(content) do
content |> Enum.join
end

Elixirといえばパターンマッチングだ! と思って書いたのがこれ.

文字列の長さをガード句の中でチェックしたかったのですが, うまい書き方が思い浮かばなかったので, いっぺん文字リストに変換してからリストの長さでマッチングを行っています.

それっぽい書き方のような気もするのですが, 文字列受け取って文字列返す関数なのに, 一度リストに変換してまた文字列に戻したりとひどく迂遠なことをやっている気がしてしょうがない.(特に, 長さ10以下の入力の場合)


再帰処理で書いてみる

  def truncate(content)  do

truncate_internal( String.graphemes(content), "")
end

def truncate_internal([head | tail], acc) do
cond do
String.length(acc) > 9 -> acc <> "..."
true -> truncate_internal(tail, acc <> head)
end
end

def truncate_internal([], acc) do
acc
end

関数型言語といえば再帰だ! という俗流理解に基づいた書き方.

書いてていい気分にはなれるのですが, いきなりこのコード見せられて, 何をやっている処理なのか

理解できる自信はないので, 仕事では書けないかなという印象.


追記

Stringモジュールには, 文字列を受け取り, {先頭文字, 残りの文字列}というタプルに変換するnext_graphemeおよびnext_codepointという関数が用意されているので

  def truncate(content)  do

truncate_internal( String.next_grapheme(content), "")
end

def truncate_internal({grapheme, tail}, acc) do
cond do
String.length(acc) > 9 -> acc <> "..."
true -> truncate_internal(String.next_grapheme(tail), acc <> grapheme)
end
end

def truncate_internal(nil, acc) do
acc
end

こう書いたほうがよいかもしれません.


とりあえずの結論

いまだどの書き方が良いかわかりませんが, (他言語経験者としての)わかりやすさ重視で

  def truncate(content) do

if String.length(content) > 10 do
String.slice(content, 0..9) <> "..."
else
content
end
end

か, そこからif文を取り除いただけの

  def truncate(content) do

cond do
String.length(content) > 10 -> String.slice(content, 0..9) <> "..."
true -> content
end
end

ですましちゃっていいような気がしてきたんですが, どう思いますか?


追記: graphemesとcodepoint

コメントで教えていただきました.

@uasiさん, @nikuさんありがとうございました.

こちらの説明がわかりやすかったのですが, 言語によっては, 複数の記号の組み合わせで一つの文字を構成する場合がある.

そういった言語の場合, UTF8では可能なすべての組み合わせにコードを振ったりするような事はせず,

複数の記号のセットとして文字を表現しているようです.

それぞれのパーツとなる記号をcodepoint, パーツのセットとして表現される文字をgraphemeと呼ぶようです。(名前からして表記の単位って意味なのかな?)

日本語の場合, こういった分離はないのでどういう時に使い分けるのか思い浮かばないのですが, 文字列を文字のリストに変換する場合は, graphemesの方を使ったほうが安全そうです.


参考サイト

[Elixir] guard構文徹底攻略 : http://qiita.com/FL4TLiN3/items/110d177175d6708322ca

5. case, condそしてif : http://elixir-ja.sena-net.works/getting_started/5.html

codepointsとgrapheme: http://joe-noh.hatenablog.com/entry/2014/07/08/065056