Подтвердить что ты не робот

Идиоматическая инкапсуляция в Clojure: как данные могут быть связаны со связанным поведением?

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

Вот пример использования OOP:

var apple = {
    type: "macintosh",
    color: "red",
    cost: 5
    markup: 1.5
    getInfo: function () {
        return this.color + ' ' + this.type + ' apple';
    }
    getPrice: function(){
        return this.cost * this.markup;
    }
}

ИЛИ аналогично:

var person = {
    birthdate: '8/30/1980',
    firstname: 'leeroy',
    middleinitial: 'b',
    lastname: 'jenkins',
    getAge: function () {
        return -(new Date()
 - new Date(this.birthdate));
    }
    getFullFormattedName: function () {
        return capitalize(this.firstname+' '+this.middleinitial+' '+this.lastname;
    }
}

Часто бывает удобно связать поведение с данными таким образом, но каков идиоматический способ, которым Clojure позволяет решить эту проблему?

4b9b3361

Ответ 1

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

; Define a record type with given fields
; `defrecord` macro defines a type and also two constructor functions,
; `->apple` and `map->apple`. The first one take a number of arguments
; corresponding to the fields, and the second one takes a map with
; field names as keys. See below for examples.
(defrecord apple [type color cost markup])

; Define several functions working on apples
; Note that these functions do not have any kind of reference to the datatype,
; they exploit map interface of the record object, accessing it like a map,
; so you can supply a real map instead of record instance, and it will work
(defn get-info [a] (str (:color a) " " (:type a) " apple"))
(defn get-price [a] (* (:cost a) (:markup a)))

; Example computation
; Bind `a` to the record created with constructor function,
; then call the functions defined above on this record and print the results
(let [a (->apple "macintosh" "red" 5 1.5)
      a-info (get-info a)
      a-price (get-price a)]
  (println a-info a-price))
; Will print the following:
; red macintosh apple 7.5

; You can also create an instance from the map
; This code is equivalent to the one above
(let [a (map->apple {:type "macintosh" :color "red" :cost 5 :markup 1.5})
      a-info (get-info a)
      a-price (get-price a)]
  (println a-info a-price))

; You can also provide plain map instead of record
(let [a {:type "macintosh" :color "red" :cost 5 :markup 1.5}
      a-info (get-info a)
      a-price (get-price a)]
  (println a-info a-price))

Обычно вы используете записи, когда вам нужен статический объект с известными полями, доступными из кода Java (defrecord генерирует правильный класс, а также множество других функций, описанных по ссылке выше), а карты используются во всех других case - аргументы ключевого слова, промежуточные структуры, динамические объекты (например, те, которые возвращаются из SQL-запроса) и т.д.

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

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

Обновить: ответ на ваш комментарий для другого ответа:

Я пытаюсь определить, какой подход заменяет методы OOP

Полезно понять, что такое "методы ООП".

Любой метод в обычном объектно-ориентированном языке, таком как Java или, в особенности, С++, по существу, является простой функцией, которая принимает неявный аргумент this. Из-за этого неявного аргумента мы считаем, что методы "принадлежат" к некоторому классу, и эти методы могут работать на объекте, на который они "вызывают".

Однако ничто не мешает вам писать дружественную глобальную функцию (в С++) или общедоступный статический метод (в Java), который берет объект в качестве первого аргумента и выполняет все, что можно сделать из метода. Никто не делает этого из-за полиморфизма, который обычно достигается с помощью понятия методов, но мы не рассматриваем его прямо сейчас.

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

Ответ 2

Создайте хэш-карту закрытий, которые имеют этот в их лексической области. Это не отличается от вашего исходного кода JavaScript.

(defn apple-object [this]
  {:get-info  #(str (this :color) " " (this :type) " apple")
   :get-price #(* (this :cost) (this :markup))})

(defn person-object [this]
  {:get-age
   #(- (-> (java.util.Date.) (.getTime))
       (-> (this :birthdate) (.getTime)))

   :get-full-formatted-name
   #(clojure.string/join
     " "
     (map clojure.string/capitalize
          [(this :firstname) (this :middleinitial) (this :lastname)]))})

;;;; usage ;;;;

(def apple (apple-object
            {:type    "macintosh"
             :color   "red"
             :cost    5
             :markup  1.5}))

(apple :type)

((apple :get-info))

((apple :get-price))


(def person (person-object
             {:birthdate     (java.util.Date. 80 7 30)
              :firstname     "leeroy"
              :middleinitial "b"
              :lastname      "jenkins"}))

(person :birth-date)

((person :get-age))

((person :get-full-formatted-name))

Ответ 3

Не делай этого. Вы пытаетесь написать ООП в FP. Это так же плохо, как написание китайского языка на английском языке.