Вопрос

Я изучаю 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» немного сложнее, чем должно быть — так много назначений переменных.Пропустил ли я какое-либо очевидное (или не столь очевидное) использование языка или библиотеки, которое сделало бы этот метод более кратким?

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

Это было полезно?

Решение

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

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

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

  • Кроме того, я бы переписал функцию 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)))
    

    Другие советы

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

    (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 возвращать список результатов, даже если он только один; это сделает вашу жизнь проще. Возможно, переименуйте его в mode , пока вы на нем.

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

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

    При этом используются следующие факты:

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

    РЕДАКТИРОВАТЬ

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

    (defn modes [data] 
      (->> data
           frequencies 
           (sort-by second)
           (partition-by second)
           last
           (map first)))
    

    Мне кажется, все в порядке.Я бы заменил

    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;это то, что нужно каждый день.)

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

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

    Лицензировано под: CC-BY-SA с атрибуция
    Не связан с StackOverflow
    scroll top