Статьи

ClojureScript не (всегда) нуждается в обезболивающем

Несколько недель назад я поделился своей путаницей по поводу написания объектно-ориентированного ClojureScript и небольшой библиотеки под названием cljs-painkiller . Благодаря удивительному сообществу Clojure / ClojureScript я скоро узнал гораздо лучшие способы сделать это.

Пример обезболивающего

Я пожаловался, что мне пришлось написать ClojureScript, который выглядит так:

(defn Bag []
  (this-as this
           (set! (.-store this) (array))
           this))
 
(set! (.. Bag -prototype -add)
      (fn [val]
        (this-as this
                 (.push (.-store this) val))))
 
(set! (.. Bag -prototype -print)
      (fn []
        (this-as this
                 (.log js/console (.-store this)))))
 
(def mybag (Bag.))
(.add mybag 5)
(.add mybag 7)
(.print mybag)

Неправильно! Вскоре после того, как эта статья появилась на DZone, Дэвид Нолен ( @swannodette ) показал мне несколько фрагментов в простом ClojureScript, которые делают то же самое:

(deftype Bag [store]
  Object
  (add [_ x] (.push store x))
  (print [_] (.log js/console store)))
 
(defn bag [arr] (Bag. arr))

	
(defn bag [store]
  (reify
    Object
    (add [this x] (.push store x))
    (print [this x] (.log js/console store))))

Намного лучше, не так ли? И он компилируется в довольно идиоматический, совместимый JavaScript, а не в какую-то магию более высокого уровня.

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

(defn bag [] (Bag. (array)))

(defn bag []
  (let [store (array)]
    (reify
      Object
      (add [this x] (.push store x))
      (print [this x] (.log js/console store)))))

Пересмотренный пример магистрали

Когда я только начинал с ClojureScript, я поделился примером интеграции с Backbone . Затем я пожаловался, что он совершенно непригоден для использования с любым менее тривиальным кодом Backbone.

Вот как выглядел мой образец:

(def MyModel
  (.extend Backbone.Model
    (js-obj
      "promptColor"
      (fn []
        (let [ css-color (js/prompt "Please enter a CSS color:")]
          (this-as this
                   (.set this (js-obj "color" css-color))))))))
 
(def my-model (MyModel.))

Оказывается, его можно переписать так:

(def MyModel
  (.extend Backbone.Model
    (reify Object
      (promptColor [this]
        (let [ css-color (js/prompt "Please enter a CSS color:")]
          (.set this (js-obj "color" css-color)))))))
 
(def my-model (MyModel.))

Много шума прошло. Похоже, что такой reifyвызов — путь в этом случае.

Спасен?

Мне нравится, когда сообщество доказывает, что это неправильно, и, очевидно, есть лучшие способы сделать это, чем я думал изначально На самом деле, когда я начал свое приключение с ClojureScript, я поссорился с компилятором — теперь я наконец начинаю понимать, что я делаю.

ClojureScript требует некоторой церемонии вокруг создания объекта, отделения поведения от состояния и его инициализации. В некоторых контекстах это слишком ограничительно, в некоторых — просто отлично.

Мой последний пример — шип Knockout, где у меня был такой JavaScript:

function AppViewModel() {
    this.firstName = ko.observable("Bert");
    this.lastName = ko.observable("Bertington");
    this.fullName = ko.computed(function() {
        return this.firstName() + " " + this.lastName();
    }, this);
    this.capitalizeLastName = function() {
        var currentVal = this.lastName();
        this.lastName(currentVal.toUpperCase());
    };
 
}
 
ko.applyBindings(new AppViewModel());

Для тех, кто не знаком с нокаутом и firstNameт. Д., Есть методы. Особенно интересными являются методы fullNameи capitalizeLastName. Здесь у нас есть метод, созданный вызовом to ko.computed, упаковывающий функцию, которая ссылается на другие методы thisобъекта. Не так уж плохо на языке OO …

… Но в ClojureScript, видимо, лучшее, что вы можете сделать, — это то, что я сделал в начале работы:

(def my-model
  (js-obj
    "firstName" (.observable js/ko "Bert")
    "lastName" (.observable js/ko "Bertington")
    "fullName" (this-as this (.computed js/ko
                 (fn []  this (.firstName this)), this))))
 
(.applyBindings js/ko my-model)

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

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