-
-
Save emanon001/4292285 to your computer and use it in GitHub Desktop.
;;; Project Euler Problem 6 | |
;;; 「二乗の和と和の二乗の差はいくつか?」 | |
;;; http://odz.sakura.ne.jp/projecteuler/index.php?cmd=read&page=Problem%206 | |
(use 'clojure.test) | |
(defn sum-of-squares | |
"与えられた数列についてその二乗の和を返します" | |
[nums] | |
(apply + (map #(* % %) nums))) | |
(defn square-of-sum | |
"与えられた数列についてその和の二乗を返します" | |
[nums] | |
(apply * (repeat 2 (apply + nums)))) | |
(defn solve | |
"最初の n 個の自然数について、二乗の和と和の二乗の差を返します" | |
[n] | |
(->> (range 1 (inc n)) | |
((juxt square-of-sum sum-of-squares)) | |
(apply -))) | |
(is (= (solve 10) 2640)) | |
(time (solve 100)) |
すみません。解説を忘れていました。
- 問題文をそのまま素直に解いた感じです。
- 同じ値を複数回参照する必要がある場合は、
let
で値を保持しない方針で解きました。(juxt
を使いたかった、という説もあります)
@kohyama
ありがとうございます。
juxt
は好きな関数なので使用してみました。
(range 1 (inc n))
を realization するのは一回だけ.let
を使わない.
という縛りなら juxt
しか思いつきません.これは個人の感想であり感じ方には個人差がありますが, ->>
の中で juxt
使うと括弧が二重になるので気持ち悪いです.そして,よく二重にするのを忘れて怒られます.
juxt
いいですね。自分はあまり使ったことないのですが、単に経験不足な気がします(他の人のソースで書かれているのを見てすげぇ〜ってよくなりますwww)。勉強になります。
juxt
おもしろいです。多値がないからこそのコンビネータですね。comp
と(あと、ある意味でmap
とも)対な感じの立ち位置が素敵です。
これは個人の感想であり感じ方には個人差がありますが, ->> の中で juxt 使うと括弧が二重になるので気持ち悪いです.そして,よく二重にするのを忘れて怒られます.
これは僕も気持ち悪いと感じています。(( ... ) x)
だとそれほどですが、(( ... ))
だと違和感をかなり感じます。
と、いうことで comp
を使って書き直しました。
(defn solve
"最初の n 個の自然数について、二乗の和と和の二乗の差を返します"
[n]
((comp (partial apply -)
(juxt square-of-sum sum-of-squares)
(partial range 1))
(inc n)))
僕はこちらの方が感覚として読みやすいです。
と、いうことで comp を使って書き直しました。
ポイントフリースタイル憧れますよね.
(let [solve (comp (partial apply -)
(juxt square-of-sum sum-of-squares)
range
inc)]
... )
という書き方はいかがでしょう.私はよくヘルパー関数をこの形でかきます.
👍 ポイントフリー万歳
ですが念のため,
初学者向け解説
「1から与えられた数 n
までの自然数の, 二乗の和と和の二乗の差はいくらか」ということなので
(- (#(* % %) (apply + (range 1 (inc n))))
(apply + (map #(* % %) (range 1 (inc n)))))
を計算すれば良いわけですが, 上記のままだと #(* % %)
と (range 1 (inc n))
をそれぞれ二回直接書いていますので, 宜しくありません.
let
を使って
(let [sq #(* % %)
ls (range 1 (inc n))]
(- (sq (apply + ls))
(apply + (map sq ls))))
とかけますが, 上記では, 他の書き方について検討しています.
特に (range 1 (inc n))
部分を let
を使わないで,
#(- (sq (apply + %) (apply + (map sq %))
に相当する関数を合成して, 引数として一度だけ渡すやり方などです.
@kohyama
私の解を読み返したところ、(range 1 (inc n))
を見事に2度書いてしまっていました。
ふと気になったのですが、同じ遅延シーケンスを2箇所で順に使おうとすると、片方で使うために計算し終えた要素すべて、もう片方で使うまでメモリに持つことになりますよね。let
でも juxt
でもきっとそうだと思います。
とすると「シーケンスの各要素の計算が高コストなのでメモリを使ってでも二度は計算したくない」のか、 「(もしかしたらものすごく長い)繰り返しの抽象化として遅延シーケンスを使っているのか」で扱いを変えたほうがよい気がします。後者でやりたい場合は「シーケンスを生成する関数」でやればよいでしょうか。
(let [sq #(* % %)
gen-ls #(range 1 (inc n))]
(- (sq (apply + (gen-ls)))
(apply + (map sq (gen-ls)))))
これを @tnoda さんの例にならって関数合成で書いてみたらこんな感じでしょうか。
(let [solve (let [sq #(* % %)
sum (partial apply +)
gen-ls #(range 1 (inc %))]
(comp (partial apply -)
(juxt (comp sq sum gen-ls)
(comp sum (partial map sq) gen-ls))
))]
...
)
もうちょっとシンプルになりそうな気もするのですが……
重要な理由として DRY (Don't Repeat Yourself) 原則があります.
同じことをするコードを複数書いてしまうと, 実装を変更する時にそれら全てを更新しなければならないので良くありません.
#(* % %)
の部分や (range 1 (inc n))
がもっと大きなコードであった場合を想定し, 一度しか書かないためのイディオムを演習しておくと良い. と思います.
その上で, 抽象化しようとしているものの性質
「シーケンスの各要素の計算が高コストなのでメモリを使ってでも二度は計算したくない」のか、 「(もしかしたらものすごく長い)繰り返しの抽象化として遅延シーケンスを使っているのか」
で扱いを変えたほうが良いというのは, なるほど. 確かにおっしゃる通りですね.
今後, 意識してみようと思います.
前者については, Problem 14 の方で話題にあがっている「メモ化」も参考になるかと思います.
juxt
は面白い関数なのですが、使うのにちょうど良い場面になかなか遭遇しない(ぼくが気づいていない)ですね。
うまく使えている例だと思います。
遠回りにはなっていないので
juxt
の使用例として良いと思います.👍