【问题标题】:Hash-map not displaying as hash-map哈希映射不显示为哈希映射
【发布时间】:2017-11-13 04:05:17
【问题描述】:

clojure 新手。尝试用java背景解决以下问题。我需要将表转换为将产品映射到销售该产品的所有城市的哈希图。所以输出应该是。

{"Pencil": ("Oshawa" "Toronto")
 "Bread": ("Ottawa" "Oshawa" "Toronto")}


(def table [
        {:product "Pencil"
        :city "Toronto"
        :year "2010"
        :sales "2653.00"}
        {:product "Pencil"
        :city "Oshawa"
        :year "2010"
        :sales "525.00"}
        {:product "Bread"
        :city "Toronto"
        :year "2010"
        :sales "136,264.00"}
        {:product "Bread"
        :city "Oshawa"
        :year "nil"
        :sales "242,634.00"}
        {:product "Bread"
        :city "Ottawa"
        :year "2011"
        :sales "426,164.00"}])

这是我目前所拥有的。我将此代码写入repl。

(let [product-cities {}]
(for [row table]
  (if (= (contains? product-cities (keyword (row :product))) true)
    (println "YAMON") ;;To do after. Add city to product if statement is true
    (into product-cities {(keyword (row :product)) (str (row :city))}))))

但是,结果如下:

({:Pencil "Toronto"}
 {:Pencil "Oshawa"}
 {:Bread "Toronto"}
 {:Bread "Oshawa"}
 {:Bread "Ottawa"})

我的 if 语句一直返回 false。我看到许多哈希映射周围都有半圆括号。我不明白为什么它没有返回一个哈希图以及为什么有很多哈希图?谢谢

编辑:

问题 2: 将表转换为将产品映射到销售额最高的城市的哈希图。例如,输出应如下所示:

{"Pencil": "Toronto"
 "Bread": "Ottawa"}

我认为需要一种不同于建立价值的策略,但我的想法如下:

(reduce (fn [product-cities {:keys [product city sales]}]
      (update-in product-cities [product] (fnil conj []) {(keyword city) sales}))
    {}
    table)

这会产生以下输出:

{"Bread"
 [{:Toronto "136,264.00"}
  {:Oshawa "242,634.00"}
   {:Ottawa "426,164.00"}],
 "Pencil"
 [{:Toronto "2653.00"}
  {:Oshawa "525.00"}]}

然后我可以再次使用 reduce 函数,但只添加销售额最高的城市。我认为这不是最有效的方法。

【问题讨论】:

    标签: clojure


    【解决方案1】:

    您似乎对 clojure 的工作原理有一些误解。来自 java 可能很难知道如何做事,因为它是如此不同。这个小问题很好地介绍了如何建立价值,我将尝试解释解决方案的每个部分。

    不变性

    定义一个保存最终结果的变量,然后在添加到该变量时循环某些内容,这是 java 中的一种常见模式。

    这就是您尝试对本地 product-cities 执行的操作。当您在 clojure 中使用 let 定义本地时,它永远不会改变,因此要建立一个值,您需要另一个模式。

    在地图上添加一些东西

    首先让我们看一下如何“向地图添加内容”。在 clojure 中,你实际上做的是用添加的东西制作一张新地图。旧地图不变。我们有时仍将其表述为添加到地图中,但这只是“使用添加的东西制作新地图”的简写。

    assoc 接受一个映射、一个键和一个值,并返回一个在键处添加值的新映射。如果那里已经有一个值,它将被覆盖。我们希望每个键都有多个东西,所以在这种情况下它不是正确的。

    update 类似,但它需要一个映射、一个键、一个函数以及该函数的可选参数。它将使用已经在 key 处的值作为第一个参数和(如果存在)提供的参数调用该函数。返回的映射将函数的返回值作为键处的新值。一些示例可能会更清楚地说明这一点。

    ;; The function - is called with the old value of :foo and the argument supplied
    ;; (- 10 3)
    (update {:foo 10} :foo - 3) ;=> {:foo 7}
    

    如果键中没有任何内容,则将使用nil 作为第一个参数调用该函数。这就是nil 的意思,没什么。

    (update {} :foo + 5) ;=> Null pointer exception. Same as (+ nil 5)
    

    空指针不好。有一个技巧可以避免它们。 fnil 是一个接受函数和参数的高阶函数。它返回一个新函数,该函数将用 nil 参数替换提供的参数。

    ;; The nil here is substituted with 0, so no NPE
    ((fnil + 0) nil 5) ;=> 5
    
    ;; If the arg is not nil, the zero is not used.
    ((fnil + 0) 5 5) ;=> 10
    
    ;; So now we can update the value at :foo regardless of whether it's already there.
    (update {} :foo (fnil + 0) 5) ;=> {:foo 5}
    

    conj 将某些内容添加到集合中。如果该集合是向量,则将其添加到末尾。

    (conj [] :foo) ;=> :foo
    
    (conj [:foo] :bar) ;=> [:foo :bar]
    

    要向地图中添加东西,我们可以结合这些:

    (update {} "product" (fnil conj []) "city") ;=> {"product ["city"]"}
    
    (update {"product" ["city"]} "product" (fnil conj []) "another city")
    ;;=> {"product" ["city" "another city"]}
    

    建立价值

    我们需要以某种方式进行一些循环。但是,clojure 中的 for 是列表理解,而不是 for 循环。它会返回一系列的东西,所以当你想建立一个值时使用它不是正确的。

    一种方法是使用loop

    通过循环,您可以定义绑定名称及其初始值。循环中的绑定可以被认为是“在循环中会发生变化的东西”。

    loop 和传统循环之间的一个区别是,要跳出循环,您根本不需要做任何事情,而您需要专门使用 recur 来保持循环。

    ;; the bindings are pairs of names and initial values
    (loop [product-cities {} ; This is the accumulator that we're gonna build up.
           rows table] ; and these are the rows, we're gonna go through them one by one.
    
      ;; Here's the base case, if there are no rows left we return the accumulator.
      (if (empty? rows)
        product-cities
    
        ;; If there are rows left, we need to add to the accumulator.
        (let [row (first rows)
              city (:city row)
              product (:product row)
              new-accumulator (update product-cities product (fnil conj []) city)]
    
          ;; recur takes as many arguments as the pairs we defined
          ;;  and "jumps" back to the loop
          (recur new-accumulator (rest rows)))))
    ;;=> {"Pencil" ["Toronto" "Oshawa"], "Bread" ["Toronto" "Oshawa" "Ottawa"]}
    

    这可以通过一些destructuring 变得更好。

    (loop [product-cities {}
           [{:keys [city product] :as row} & rows] table]
      (if (nil? row)
        product-cities
        (recur (update product-cities product (fnil conj []) city) rows)))
    ;;=> {"Pencil" ["Toronto" "Oshawa"], "Bread" ["Toronto" "Oshawa" "Ottawa"]}
    

    loop 在普通的 clojure 中使用不多。它太笼统了,通常你想要更具体的东西。就像在这种情况下,您希望在循环遍历一系列事物中的每个事物时建立一个值。这就是reduce 所做的。

    Reduce 接受三个参数,一个函数、一个初始值和一个集合。该函数称为“归约函数”,有两个参数;到目前为止的累计值和一个项目。它为集合中的每个项目调用一次。

    所以最终的实现变成了:

    (reduce (fn [product-cities {:keys [product city]}]
              (update product-cities product (fnil conj []) city))
            {}
            table)
    

    编辑:

    关于您对另一个答案的评论。 update 是在 clojure 1.7.0 中添加的,因此您可以使用旧版本。您可以改用update-in(尽管您应该考虑升级)。除了键在向量中之外,它的调用方式完全相同。

    (reduce (fn [product-cities {:keys [product city]}]
              (update-in product-cities [product] (fnil conj []) city))
            {}
            table)
    

    【讨论】:

    • 我的comfy 库有一个group-by 版本,它可以更简洁地做同样的事情:(comfy/group-by :product (map :city) table)。不过它使用了传感器,这可能是一个最好在学习过程中稍后再讨论的主题。
    • 谢谢你。绝对有帮助,但我认为我需要更多时间来完全掌握这些概念。我已经添加了另一个问题,如果你能好心地帮助我。我认为需要采用不同的策略,因为我们这次要找到最大值。
    【解决方案2】:

    你需要一一翻表,积累(conj)产品下的城市:

    (reduce
        (fn [acc {:keys [product city]}]
          (update acc product conj city))
        {}
        table)
    ;; => {"Pencil" ("Oshawa" "Toronto"), "Bread" ("Ottawa" "Oshawa" "Toronto")}
    

    更新函数(本例中为conj)的使用可能有点棘手,因此这里是update 的另一种形式:

    (update acc product (fn [cities] 
                          (conj cities city)))
    

    我从{}开始:

    {"Pencil" []
     "Bread" []}
    

    这可能更容易看出update 正在通过将最新城市放在末尾来更新产品密钥(“铅笔”或“面包”) (这就是conj 所做的)序列。当它工作时,我只是用{} 替换,使用update 将插入一个新密钥(如果不存在)这一事实。

    我认为for 具有生成性。它需要一个序列作为输入,并且在每一步都会生成一个新事物——因此你最终会得到一系列新事物。 generation 无法更新product-cities

    reduce 对于您想要做的事情更有用,因为您获得了一个可以在每个步骤中稍微修改的“累加器”。实际上,您每次都在通过修改传入的“累加器”来创建一个新的“累加器”,因此实际上您根本没有修改任何东西:Clojure 是一种函数式语言,它的全部目的是创建新事物。

    【讨论】:

    • 最好详细说明for 不是 for 循环以及“定义一个变量,然后在更新它的同时循环某些内容”通常是用reduce 完成的。
    • 尝试运行代码时出现以下错误。CompilerException java.lang.RuntimeException: Unable to resolve symbol: update in this context, compile:
    • 是的,那是因为您使用的是旧版本的 clojure。看看我回答的结尾,看看你如何使用update-in 来做同样的事情。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-09-22
    • 1970-01-01
    • 2018-10-13
    • 1970-01-01
    • 2016-04-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多