Open4

学習記録:ElmでCodewars

えとあるえとある

このスクラップについて

Elmを学び始めたが、ボクはこれまでほぼElmを書いたことがない。Elmの基本文法とコーディングスタイルに慣れるため、Codewars のコーディング課題 (Kata) をElmで解いていく[1]

https://www.codewars.com

このスクラップには自分が提出したコードとそのkataを通して学んだことを振り返ってログとして残していく[2]。断っておくとボクはElmおよび純粋関数型言語に入門したてのド素人なので、書き方は全然他の方の参考にはならないと思っていてほしい。

脚注
  1. CodewarsでのElmは対応言語としてはまだBeta版のようで、問題数はそこまで多くない。 ↩︎

  2. 気軽に「もっとうまい書き方があるよ!」というご指摘をいただけるように一応このスクラップでは「他のユーザーの投稿を許可」ONとしているが、CodewarsはElmの参加人口がまだまだ少ないようなのでどうせなら登録してご自分で解答を提出することをオススメする。なぜならCodewarsでは1度正解のコードを提出できれば他の方の解答を見ることができ、かつBest practiceCleverという2つの軸で解答に投票できるからだ。 ↩︎

えとあるえとある

[7 kyu] Strong Number (Special Numbers Series #2)

https://www.codewars.com/kata/5a4d303f880385399b000001

与えられた正の整数がstrong numberかどうかを判定する問題。Strong numberとは、各桁の数字の階乗の和が自身と等しくなるような数のこと。たとえば145はstrong numberである。

145 = 1! + 4! + 5! = 1 + 24 + 120

自分の解答

strong : Int -> String
strong n =
  if (isStrong n) then
    "STRONG!!!!"
  else
    "Not Strong !!"

isStrong : Int -> Bool
isStrong n =
  String.fromInt n
    |> String.toList
    |> List.map (String.fromChar >> String.toInt >> Maybe.withDefault 0 >> factorial)
    |> List.sum
    |> (==) n

factorial : Int -> Int
factorial n =
  case n of
    0 ->
      1
    _ ->
      n * factorial (n - 1)

学んだこと・振り返り

  • パイプ演算子 |> の使い方
    • 直前の値を1つ、パイプで繋いだ先の関数の引数として渡せる
    • 🤔💭(2つ以上の値は渡せないのかな…?)
  • ヘルパ関数への切り出し
    • 切り出しの粒度が適切かどうかはまだ判断できない
    • Elm Guide 曰く、部分適用が長くなってたらヘルパ関数へ切り出した方が良いとのこと
      • isStrongはちょっと長すぎかも…?
  • List.map のelement-wiseな処理を関数合成 >> でまとめてしまったけど、一般的な手法なのかは不明
えとあるえとある

[6 kyu] Statistics for an Athletic Association

https://www.codewars.com/kata/55b3425df71c1201a800009c

時間|分|秒 というフォーマットで表される複数のタイムの値域 Range、平均 Average、中央値 Medianを求めて、指定の文字列フォーマットで出力する問題。

自分の解答

stat: String -> String
stat s =
  let
    range = listSeconds s |> calcRange |> seconds2hms
    average = listSeconds s |> calcAverage |> seconds2hms
    median = listSeconds s |> calcMedian |> seconds2hms
  in
    if String.length s == 0
    then
      ""
    else
      String.join " " ["Range:", range, "Average:", average, "Median:", median]


listSeconds: String -> List Int
listSeconds s = String.split "," s |> List.map (String.trim >> hms2seconds)

hms2seconds: String -> Int
hms2seconds hms =
  let
    conv s = String.toInt s |> Maybe.withDefault 0
  in
    String.split "|" hms
      |> List.indexedMap (\i s -> if i == 0 then (conv s)*3600 else if i == 1 then (conv s)*60 else conv s)
      |> List.sum


seconds2hms: Int -> String
seconds2hms sec =
  let
    h = sec // 3600
    m = (modBy 3600 sec) // 60
    s = modBy 60 sec
    conv i = String.fromInt i |> String.padLeft 2 '0'
  in
    String.join "|" [(conv h), (conv m), (conv s)]
    

calcRange: List Int -> Int
calcRange list =
  let
    max = List.maximum list |> Maybe.withDefault 0
    min = List.minimum list |> Maybe.withDefault 0
  in
    max - min


calcAverage: List Int -> Int
calcAverage list =
  (List.sum list) // (List.length list)


calcMedian: List Int -> Int
calcMedian list =
  let
    len = List.length list
  in
    if modBy 2 len /= 0 then
      List.sort list
        |> List.drop (len // 2)
        |> List.head
        |> Maybe.withDefault 0
    else
      List.sort list
        |> List.drop (len // 2 - 1)
        |> List.take 2
        |> calcAverage

学んだこと・振り返り

  • let式の使い方
    • 関数内の一時変数を作れるので、従来手続き型でコーディングしていた身には優しい仕様
    • let式の中で定義した一時変数を同じlet式の別の行で使えるということは、let式の実行順序は上→下ということなのかな…?
  • とても単純なことをしているだけのはずなのに、コード長くない? こんなもん…?
えとあるえとある

let式の実行順序は上→下ということなのかな…?

結論: 順序は関係ない(多分)

Elm REPLで検証

以下は簡単な例。もっと順序がクリティカルになる状況があるかもしれない。

> f =
|   let
|     a = b + 1  -- 後で定義する`b`をここで使う
|     b = 2
|   in
|     a * b
|
6 : number       -- ✅
> f =
|   let
|     c = d + 1  -- `c`と`d`を循環的に使う
|     d = c + 1
|   in
|     c * d
|
-- CYCLIC VALUE ----------------------------------------------------------- REPL

I do not allow cyclic values in `let` expressions.

4|     c = d + 1
       ^
The `c` value depends on itself through the following chain of definitions:

    ┌─────┐
    │    c
    │     ↓
    │    d
    └─────┘