Функция идиоматического режима в Clojure - PullRequest
8 голосов
/ 21 октября 2009

Я изучаю Clojure и хотел бы получить совет по идиоматическому использованию. В рамках небольшого пакета статистики у меня есть функция для расчета режима набора данных. (Справочная информация: режим является наиболее распространенным значением в наборе данных. Существует почти дюжина опубликованных алгоритмов для его расчета. Один из используемых здесь взят из "Основ биостатистики", 6-е издание Бернарда Рознера.)

(defn tally-map
 " Create a map where the keys are all of the unique elements in the input
   sequence and the values represent the number of times those elements
   occur. Note that the keys may not be formatted as conventional Clojure
   keys, i.e. a colon preceding a symbol."
  [aseq]
  (apply merge-with + (map (fn [x] {x 1}) aseq)))

(defn mode
 " Calculate the mode. Rosner p. 13. The mode is problematic in that it may
   not be unique and may not exist at all for a particular group of data.
   If there is a single unique mode, it is returned. If there are multiple
   modes, they are returned as a list. If there is no mode, that is all
   elements are present in equal frequency, nil is returned."
  [aseq]
  (let [amap (tally-map aseq)
        mx (apply max (vals amap))
        k (keys amap)
        f (fn [x] (not (nil? x)))
        modes (filter f (map #(if (= mx (get amap %)) %) k))
        ]
    (cond (= 1 (count modes)) (first modes)
      (every? #(= mx %) (vals amap)) nil
      :else modes)
    )
  )

У меня есть несколько вопросов по поводу:

  1. Аргумент. Функция принимает одну последовательность. Является ли более идиоматичным принимать переменное число аргументов, таких как функция сложения?
  2. Код запаха. Кажется, что «let» немного сложнее, чем должно быть - так много переменных назначений. Я пропустил какое-либо очевидное (или не столь очевидное) использование языка или библиотеки, которое сделало бы этот метод более кратким?

Заранее спасибо за помощь.

Ответы [ 4 ]

5 голосов
/ 21 октября 2009

По моему мнению, отображение какой-либо функции над коллекцией, а затем немедленное сжатие списка до одного элемента - признак использования reduce.

(defn tally-map [coll]
  (reduce (fn [h n]
            (assoc h n (inc (h n 0))))
          {} coll))

В этом случае я бы написал mode fn, чтобы взять в качестве аргумента одну коллекцию, как вы и сделали. Единственная причина, по которой я могу использовать несколько аргументов для такой функции, заключается в том, что вы планируете много печатать буквальные аргументы.

Так, например, если это для интерактивного сценария REPL, и вы часто будете буквально печатать (mode [1 2 1 2 3]), тогда у вас должна быть функция, принимающая несколько аргументов, чтобы вы не набирали дополнительный [] в вызове функции все время. Если вы планируете читать множество чисел из файла, а затем использовать режим этих чисел, тогда пусть функция принимает единственный аргумент, который является коллекцией, чтобы вы могли избежать использования apply все время. Я предполагаю, что ваш самый распространенный вариант использования - последний. Я считаю, что apply также добавляет издержки, которых вы избегаете, когда у вас есть вызов функции, который принимает аргумент коллекции.

Я согласен с другими, что вы должны mode вернуть список результатов, даже если он только один; это сделает вашу жизнь проще. Может быть, переименуйте его modes, пока вы там.

4 голосов
/ 19 августа 2012

Вот хорошая краткая реализация mode:

(defn mode [data] 
  (first (last (sort-by second (frequencies data)))))

Это использует следующие факты:

  • Функция frequencies возвращает карту значений -> частоты
  • Вы можете рассматривать карту как последовательность пар ключ-значение
  • Если вы отсортируете эту последовательность по значению (элемент second в каждой паре), то последний элемент в последовательности будет представлять режим

EDIT

Если вы хотите обработать случай нескольких режимов, вы можете вставить дополнительный partition-by, чтобы сохранить все значения с максимальной частотой:

(defn modes [data] 
  (->> data
       frequencies 
       (sort-by second)
       (partition-by second)
       last
       (map first)))
4 голосов
/ 21 октября 2009

Вот мой дубль:

  1. Есть много основных функций clojure, которые принимают последовательности в качестве аргументов, в то время как другие принимают несколько аргументов, поэтому, на мой взгляд, нет никакого идиоматического способа. Если у вас уже есть данные в последовательности, я бы использовал seq в качестве аргумента, так как это избавит вас от необходимости применять.

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

Кроме того, я бы переписал функцию mode следующим образом:

(defn mode [aseq]
  (let [amap (tally-map aseq)
        mx (apply max (vals amap))
        modes (map key (filter #(= mx (val %)) amap))
        c (count modes)]
    (cond
      (= c 1) (first modes)
      (= c (count amap)) nil
      :default modes)))

Вместо определения функции f вы можете использовать функцию идентификации (если ваши данные не содержат значений, которые являются логически ложными). Но тебе это даже не нужно. Я нахожу режимы по-другому, что для меня более читабельно: карта amap действует как последовательность записей карты (пары ключ-значение). Сначала я фильтрую только те записи, которые имеют значение mx. Затем я сопоставляю функцию клавиш с ними, давая мне последовательность клавиш.

Чтобы проверить, есть ли какие-либо режимы, я больше не зацикливаюсь на карте. Вместо этого я просто сравниваю количество режимов с количеством записей на карте. Если они равны, все элементы имеют одинаковую частоту!

Вот функция, которая всегда возвращает seq:

(defn modes [aseq]
  (let [amap (tally-map aseq)
        mx (apply max (vals amap))
        modes (map key (filter #(= mx (val %)) amap))]
    (when (< (count modes) (count amap)) modes)))
2 голосов
/ 21 октября 2009

выглядит хорошо для меня. Я бы заменил

f (fn [x] (not (nil? x)))
mode (filter f (map #(if (= mx (get amap %)) %) k))

с

mode (remove nil? (map #(if (= mx (get amap %)) %) k))

(я не знаю, почему что-то вроде not-nil? отсутствует в clojure.core; это то, что нужно каждый день.)

Если существует один уникальный режим, он возвращается. Если есть несколько режимов, они возвращаются в виде списка. Если режима нет, то есть все элементы присутствуют с одинаковой частотой, возвращается ноль. "

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...