Clojure / FP: применять функции к каждому аргументу оператора - PullRequest
0 голосов
/ 26 июня 2018

Допустим, у меня есть несколько векторов

(def coll-a [{:name "foo"} ...])
(def coll-b [{:name "foo"} ...])
(def coll-c [{:name "foo"} ...])

, и я хотел бы видеть, равны ли имена первых элементов.

Я мог бы

(= (:name (first coll-a)) (:name (first coll-b)) (:name (first coll-c)))

, но это быстро становится утомительным и слишком многословным, так как составляется все больше функций.(Может быть, я хочу сравнить последнюю букву имени первого элемента?)

Чтобы напрямую выразить суть вычислений, кажется интуитивно понятным

(apply = (map (comp :name first) [coll-a coll-b coll-c]))

, но это заставляет меня задуматься, есть лиабстракция более высокого уровня для такого рода вещей.

Я часто нахожу себя сравнивающим / иным образом работающим с вещами, которые должны быть вычислены с помощью одной композиции, примененной к нескольким элементам, но синтаксис карты выглядит мне немного не по вкусу.

Если бы я былдля домашнего приготовления какого-то оператора, я бы хотел синтаксис, такой как

(-op- (= :name first) coll-a coll-b coll-c)

, потому что большая часть вычислений выражается в (= :name first).

Я хотел бы применить абстракцию ки оператор, и функции, применяемые к каждому аргументу.То есть сумма должна быть такой же простой, как и сумма сравнения.

(def coll-a [{:name "foo" :age 43}])
(def coll-b [{:name "foo" :age 35}])
(def coll-c [{:name "foo" :age 28}])

(-op- (+ :age first) coll-a coll-b coll-c)
; => 106
(-op- (= :name first) coll-a coll-b coll-c)
; => true

Что-то вроде

(defmacro -op- 
  [[op & to-comp] & args]
  (let [args' (map (fn [a] `((comp ~@to-comp) ~a)) args)]
    `(~op ~@args')))
  • Есть ли идиоматический способ сделать это в укорачивании, какой-то стандартныйбиблиотечную функцию, которую я мог бы использовать?
  • Есть ли имя для этого типа выражения?

Ответы [ 4 ]

0 голосов
/ 28 июня 2018

Я бы разделил этот процесс на три этапа:

  1. преобразование элементов в коллекциях в данные в коллекциях, с которыми вы хотите работать - (map :name coll);
  2. Работа с преобразованными элементамив коллекциях, возвращая коллекцию результатов - (map = transf-coll-a transf-coll-b transf-coll-c)
  3. Наконец, выбирая, какой результат в результирующей коллекции возвращать - (first calculated-coll)

При игре с коллекциями я стараюсь большечем один элемент в коллекции:

(def coll-a [{:name "foo" :age 43} {:name "bar" :age 45}])
(def coll-b [{:name "foo" :age 35} {:name "bar" :age 37}])
(def coll-c [{:name "foo" :age 28} {:name "bra" :age 30}])

Например, сопоставление элементов по второму символу в: имя и возвращаемый результат для элементов на втором месте:

(let
  [colls [coll-a coll-b coll-c]
   transf-fn (comp #(nth % 1) :name)
   op =
   fetch second]
  (fetch (apply map op (map #(map transf-fn %) colls))))
;; => false 

В мире преобразователей вы можете использовать sequence функция, которая также работает с несколькими коллекциями:

(let
  [colls [coll-a coll-b coll-c]
   transf-fn (comp (map :name) (map #(nth % 1)))
   op =
   fetch second]
  (fetch (apply sequence (map op) (map #(sequence transf-fn %) colls))))

Рассчитать сумму возрастов (для всех предметов на одном уровне):

(let
  [colls [coll-a coll-b coll-c]
   transf-fn (comp (map :age))
   op +
   fetch identity]
  (fetch (apply sequence (map op) (map #(sequence transf-fn %) colls))))
;; => (106 112)
0 голосов
/ 26 июня 2018

Для вашего примера сложения я часто использую transduce:

(transduce
  (map (comp :age first))
  +
  [coll-a coll-b coll-c])

Ваш вариант использования равенства более сложен, но вы можете создать собственную сокращающую функцию для поддержания аналогичного шаблона.Вот одна из таких функций:

(defn all? [f]
  (let [prev (volatile! ::no-value)]
    (fn
      ([] true)
      ([result] result)
      ([result item]
       (if (or (= ::no-value @prev)
               (f @prev item))
         (do
           (vreset! prev item)
           true)
         (reduced false))))))

Затем используйте ее как

(transduce
  (map (comp :name first))
  (all? =)
  [coll-a coll-b coll-c])

Семантика довольно похожа на ваш макрос -op-, но при этом является более идиоматическим Clojure и более расширяемым.Другие разработчики Clojure сразу поймут, что вы используете transduce.Возможно, им придется исследовать пользовательскую функцию сокращения, но такие функции достаточно распространены в Clojure, чтобы читатели могли увидеть, как она соответствует существующему шаблону.Кроме того, должно быть довольно прозрачно, как создавать новые сокращающие функции для случаев использования, когда простое сопоставление и применение не будет работать.Функция преобразования также может быть составлена ​​с другими преобразованиями, такими как filter и mapcat, для случаев, когда у вас есть более сложная структура исходных данных.

0 голосов
/ 26 июня 2018

Я не думаю, что вам нужен макрос, вам просто нужно параметризовать вашу op функцию и compare функции.На мой взгляд, вы довольно близки с вашей (apply = (map (comp :name first) [coll-a coll-b coll-c])) версией.

Вот один из способов сделать его более универсальным:

(defn compare-in [op to-compare & args]
  (apply op (map #(get-in % to-compare) args)))

(compare-in + [0 :age] coll-a coll-b coll-c)
(compare-in = [0 :name] coll-a coll-b coll-c)
;; compares last element of "foo"  
(compare-in = [0 :name 2] coll-a coll-b coll-c)

Я даже не знал, что вы можете использовать get в строках, но в третьем случае вы можете видеть, что мы сравниваем последний элемент каждого foo.

Этот подход не позволяет аргументам to-compare быть произвольными функциями, но кажется, что ваш сценарий использования в основном касается поиска элементов, которые вы хотите сравнить, и затем применения произвольной функции к этим значениям.,

Я не уверен, что этот подход лучше, чем версия преобразователя, представленная выше (конечно, не такая эффективная), но я думаю, что он обеспечивает более простую альтернативу, когда такая эффективность не нужна.

0 голосов
/ 26 июня 2018

Возможно, вы ищете функцию every?, но я бы улучшил ясность, разбив ее и назвав подэлементы:

  (let [colls           [coll-a coll-b coll-c]
        first-name      (fn [coll] (:name (first coll)))
        names           (map first-name colls)
        tgt-name        (first-name coll-a)
        all-names-equal (every? #(= tgt-name %) names)]

all-names-equal => true

Я бы избегал DSL, так какв этом нет необходимости, и другим становится намного труднее читать (так как они не знают DSL).Сохраняйте это простым:

  (let [colls  [coll-a coll-b coll-c]
        vals   (map #(:age (first %)) colls)
        result (apply + vals)]

result => 106
...