我在学克洛尔。不久前,我给孩子们开了一个python银行账户,教他们存钱。看看这里。
Python是一个类,它创建了一个表,该表具有一个小的兴趣函数,它显示了如果它位于那里,他们的钱会做些什么。不用说,青少年甚至对15%的年利息也不感兴趣。他们现在就想要现金。
我现在正在Clojure中实现同样的功能。我还没加利息呢。因此,首先,我要求就如何做到这一点提出建议,第二件事将是对这种方法的任何批评。我的逻辑可能是错的,但功能确实起作用了。它的功能是:平衡、事务、报告(带有pprint表)。
(ns kids-bank.core
(:require [clojure.string :as cstr])
(:import java.util.Date)
(:import java.io.File)
(:require [clojure-csv.core :as csv])
(:require [java-time :as jt])
(:require [clojure.pprint :as pp]))
(def now jt/local-date)从github复制代码片段,java.io似乎只适用于use,即使它是在ns中导入的。
(use 'clojure.java.io)(defn create-account
"starts a new csv file using name of holder in the designated folder
using clojure.java.io checks if file is there to prevent erasing
existing file"
[name]
(let [file-name (str "path/to/folder" name "-account.csv")]
(if (.exists (file file-name))
(println "Account already exists")
(do
(.createNewFile (file file-name))
(with-open [wrtr (writer (file file-name))]
(.write wrtr "date,amount,reference \n")) (println "That's done for you!"))))) 创建帐户的
(do (create-account "noah")
(create-account "robin")
(create-account "bea"))
(def robin-account "path/to/robin-account.csv")
(def bea-account "/path/to/bea-account.csv")
(def noah-account "/path/to/noah-account.csv")(defn transaction ;TODO add an optional arg for date, default today.
"accepts string for file name, amount, and reference as a string
and adds the transaction to the account"
[file n ref]
(with-open [wrtr (writer file :append true)]
(.write wrtr (str (local-date (now)) ", " n ", " ref "\n"))))我从@mo字节修改了一些解析csv函数,以便有一个默认的参数和带有或不带标题的解析选项。
(defn take-csv
"takes file name and reads data.
by default skips the first header
change key :header to true in order to include it"
[& {:keys [fname header] :or {header false}}]
(if (= header false)
(rest (with-open [file (reader fname)]
(doall (map (comp first csv/parse-csv) (line-seq file)))))
(with-open [file (reader fname)]
(doall (map (comp first csv/parse-csv) (line-seq file))))))我需要在map函数中使用来自Java的Double,并发现我需要定义一个Clojure函数,以便在map中使用fn参数,因此很少使用预代码(这就是实现它的方法吗?)
(defn dble [a]
"takes a string returns a double"
(Double. a))以及:
(defn balance
"accepts string for a file name returns the balance"
[file]
(reduce + (map dble (map second (take-csv :fname file :header false)))))以表的形式印在表格上:
(use 'clojure.pprint)在这种情况下,use似乎也是必要的(如果有人有,请澄清一下)。
(defn pretty-report
"returns a table of history of account"
[fname]
(let [header (first (take-csv :fname fname :header true))]
(loop [body (take-csv :fname fname :header false) result []]
(if (empty? body)
(do (println (str fname)) (pp/print-table result) (println
(str "the balance is: " (balance fname))))
(recur
(rest body)
(conj result (zipmap (map keyword header) (first body))))))))一切似乎都正常,下面就是一个例子:
kids-bank.core> (create-account "test")
That's done for you!
kids-bank.core> (def test-account "path/to/test-account.csv")
#'kids-bank.core/test-account
kids-bank.core> (transaction test-account 10.0 "allowence")
nil
kids-bank.core> (transaction test-account 5.0 "repayment")
nil
kids-bank.core> (transaction test-account 20.0 "from grandad")
nil
kids-bank.core> (transaction test-account -13.0 "shopping")
nil
kids-bank.core> (balance test-account)
22.0
kids-bank.core> (pretty-report test-account)
/path/to/test-account.csv
| :date | :amount | :reference |
|------------+---------+---------------|
| 2018-05-24 | 10.0 | allowence |
| 2018-05-24 | 5.0 | repayment |
| 2018-05-24 | 20.0 | from grandad |
| 2018-05-24 | -13.0 | shopping |
the balance is: 22.0
nil发布于 2018-05-24 18:02:10
这不是坏代码。我所看到的最糟糕的事情是,您的许多代码都是通过副作用操作的。
您可以直接使用take-csv从文件中读取,也可以将副作用作为函数的目的,比如使用transaction或pretty-report。你的所有功能都做得太多了,我会再把它们都分解一下。您必须在某个地方使用副作用,只需确保在可行的情况下显式地传递/返回数据,然后使用这些数据操作函数来帮助实现这些效果。通过这种方式,您可以测试数据的处理,例如,将数据写入磁盘。
我还要提出几点挑剔的观点:
你们的进口不必要地冗长。你可以把所有的:requires和:imports浓缩在一起。
(ns kids-bank.core
(:require [clojure.string :as cstr]
[clojure-csv.core :as csv]
[java-time :as jt]
[clojure.pprint :as pp])
(:import [java.util.Date]
[java.io.File]))(doall (map (comp first csv/parse-csv) (line-seq file)))可以说写得很奇怪。我只想在这里使用mapv来消除对doall的需求。如果您想要懒惰,请使用map,它将返回一个惰性序列。但是,如果您想严格计算所有内容,mapv将返回一个向量。
(mapv (comp first csv/parse-csv) (line-seq file))对于(Double. a),我个人更喜欢
(Double/parseDouble a)我觉得它更明确了一点。不过,它与parseDouble完全相同,因为构造函数只是将其委托给。
通常被认为是惯用的求和方法是
(apply + nums)而不是使用reduce。这是因为+有一个过载,这是对参数的手动还原。不过,它们基本上是一样的。
不过,在同一领域,我认为您的双map可以使用comp和/或->>进行清理。
(reduce + (map (comp dble second) (take-csv :fname file :header false)))或者,我个人喜欢的
(->> (take-csv :fname file :header false)
(map second)
(map dble)
(apply +))这给了你一个很好的自上而下的阅读。
实际上,您不应该将多个loop累加器推到一行上。这就像在其他语言中在同一行中声明多个变量一样。这是一种通常被认为会损害可读性的实践,我认为这里肯定是这样的。变化
(loop [body (take-csv :fname fname :header false) result []]至
(loop [body (take-csv :fname fname :header false)
result []]但是,在同样的部分中,您一直在first和rest上调用body。从一开始就解构body。我还将扩展您的打印调用,因为您再次尝试将过多的内容放在一行上,并切换到when以消除对do的需求。注意,x和xs是很糟糕的名字,但我不知道该如何称呼它们。我不是在这里提倡一个字母的名字。
(defn pretty-report
"returns a table of history of account"
[fname]
(let [header (first (take-csv :fname fname :header true))]
(loop [[x & xs] (take-csv :fname fname :header false)
result []]
(when x
(println (str fname))
(pp/print-table result)
(println (str "the balance is: " (balance fname)))
(recur xs
(conj result (zipmap (map keyword header) x)))))))注意,(when x可以工作,因为当序列为空时,x默认为nil。
不过,这整个部分只是对take-csv返回的内容的一个简化。它可以写成
(defn pretty-report
"returns a table of history of account"
[fname]
(let [header (first (take-csv :fname fname :header true))]
(reduce (fn [result x]
(println (str fname))
(pp/print-table result)
(println (str "the balance is: " (balance fname)))
(conj result (zipmap (map keyword header) x)))
[] ; Starting result
(take-csv :fname fname :header false))))... :or {header false}}部分在take-csv中是不必要的。如果没有提供解构,header已经默认为nil,这就是falsey。
最好不要使用ref这个名字。这已经是一个核心功能了,并且由于阴影太差而导致错误。
(if (= header false) ...)并不理想。我要么用
(if-not header ...)或者只是颠倒身体的顺序然后用
(if header ...)https://codereview.stackexchange.com/questions/195103
复制相似问题