Как использовать полиморфизм в функциональном программировании?

Как использовать полиморфизм в функциональном программировании (с системой динамического типа)?

Рассмотрим следующий пример (сначала в ООП второй в FP). Программа очень проста - есть список цифр, и нам нужно рисовать их все, разные цифры используют разные алгоритмы рисования.

В ООП это можно сделать тривиально, но как это сделать в FP? Особенно на языках с системой динамического типа, например Scheme, Clojure (без разрешения статического типа во время компиляции)?

Я создал простой код (live version http://tinkerbin.com/0C3y8D9Z, нажмите кнопку "Запустить" ). Я использовал, если /else переключился на выборку FP, но это очень плохой подход. Как такая проблема может быть решена лучше?

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

OOP

var print = function(message){document.write(message + "\n<br/>")}

// Object Oriented Approach.
var circle = {
  draw: function(){print("drawing circle ...")}
}
var rectangle = {
  draw: function(){print("drawing rectangle ...")}
}

var objects = [circle, rectangle]
objects.forEach(function(o){
  o.draw()
})

FP

var print = function(message){document.write(message + "\n<br/>")}

// Functional Approach.
var circle = {type: 'Circle'}
var drawCircle = function(){print("drawing circle ...")}

var rectangle = {type: 'Rectangle'}
var drawRectangle = function(){print("drawing rectangle ...")}

var objects = [circle, rectangle]
objects.forEach(function(o){
  if(o.type == 'Circle') drawCircle(o)
  else if(o.type == 'Rectangle') drawRectangle(o)
  else throw new Error('unknown type!')
})

Ответ 1

Полиморфизм ОО не является частью функционального программирования. Однако некоторые функциональные языки (например, clojure) имеют полиморфизм оо.

Другим видом полиморфизма являются мультиметоды

(def circle {:type :circle
             :radius 50})

(def rectangle {:type :rectangle
                :width 5
                :height 10})

(defmulti draw :type)

(defmethod draw :circle [object]
  (println "circle: radius = " (:radius object)))

(defmethod draw :rectangle [object]
  (println "rectangle: "
           "width = " (:width object)
           "height = " (:height object)))

(doseq [o [rectangle circle]] (draw o))
=> rectangle:  width =  5 height =  10
   circle: radius =  50

Или вы просто можете использовать функциональный стиль

(defn circle [] (println "drawing circle ..."))
(defn rectangle [] (println "drawing rectangle ..."))

(def objects [circle rectangle])

(doseq [o objects] (o))
=> drawing circle ...
   drawing rectangle ...

Ответ 2

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

var circle = {type: 'Circle'}
var rectangle = {type: 'Rectangle'}

var draw = function(shape) {
  switch (shape.type) {
    case 'Circle': print("drawing circle ..."); break
    case 'Rectangle': print("drawing rectangle ..."); break
  }
}

var objects = [circle, rectangle]
objects.forEach(draw)

(Конечно, это JavaScript. На функциональном языке у вас обычно есть гораздо более элегантный и сжатый синтаксис для этого, например:

draw `Circle    = print "drawing circle..."
draw `Rectangle = print "drawing rectangle..."

objects = [`Circle, `Rectangle]
foreach draw objects

)

Теперь средний поклонник OO увидит вышеприведенный код и скажет: "Но решение OO расширяемо, это не так!" Это верно в том смысле, что вы можете легко добавлять новые формы в версию OO и не должны касаться ни одного из существующих (или их функций draw), когда вы это делаете. С помощью способа FP вам нужно будет войти и расширить функцию draw и все другие операции, которые могут существовать.

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

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

Существуют и другие преимущества функциональной версии. Например, он тривиально масштабируется для многодисциплинарных или более сложных случаев. Это также предпочтительнее при реализации сложного алгоритма, где разные случаи взаимосвязаны, так что вы хотите иметь код в одном месте. Как правило, всякий раз, когда вы начинаете использовать шаблон посетителя в OO, решение функционального стиля было бы более подходящим (и намного проще).

Некоторые дополнительные замечания:

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

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

  • У вашего вопроса очень мало общего с типами. Идиоматическое OO и идиоматическое решение FP работают на нетипизированном или типизированном языке.

Ответ 3

В Clojure существуют протоколы, которые предоставляют в основном тот же ad-hoc-полиморфизм, что и классы типа Haskell:

(defprotocol shape (draw [e]))
(defrecord circle [radius])
(defrecord rectangle [w h])
(extend-protocol shape 
    circle (draw [_] "I am a nice circle")
    rectangle (draw [_] "Can I haz cornerz please?"))

Вы также можете расширить существующий тип:

(extend-protocol shape 
   String (draw [_] "I am not a shape, but who cares?"))

И затем вы можете применить метод draw к некоторым экземплярам

user=> (map draw [(->circle 1) (->rectangle 4 2) "foo"])
("I am a nice circle" "Can I haz cornerz please?" "I am not a shape, but who cares?")

Ответ 4

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

В вашем простом примере, где есть только одна функция, вы можете просто создать список функций напрямую, например objects = [drawCircle, drawRectangle].

Ответ 5

На нескольких языках, предназначенных в основном для функционального программирования, существуют способы достижения (ad-hoc, как его называют) полиморфизма, хотя они отличаются от того, что вы называете полиморфизмом. Например, Haskell имеет классы классов (не путать с классами из классического ООП):

class Draw a where
    draw :: a -> SomethingSomthing -- probably IO () for your example, btw

(Scala имеет объекты, а также implicits, которые, по-видимому, параллельны или даже превосходят классы типов.) Затем вы можете реализовать любое количество независимых типов и сделать каждый экземпляр класса типа (снова независимо, например, в совершенно другом модуле):

data Circle = Circle Point Double -- center, radius
data Rectangle = Rect Point Double Double -- center, height, width

instance Draw Circle where
    draw (Circle center radius) = …
instance Draw Rectangle where
    draw (Rect center height width) = …

Это, вероятно, то, что вы бы использовали в Haskell, если вам действительно нужна эта степень расширяемости. Если у вас есть конечное число случаев, принадлежащих вместе (т.е. Вы можете использовать классы sealed в альтернативе ООП), вы, вероятно, будете использовать алгебраические типы данных (см. Ниже).

Другой способ - сделать то, что делает ваш JS-фрагмент (что, кстати, не то, что вы сделали бы для достижения полиморфизма, если бы у вас было какое-то количество объектов каждого типа, и эта версия имеет ту же проблему): Вставить функцию, которая выполняет полиморфное поведение в каждом объекте. В некотором смысле ваш фрагмент "ООП" уже функционирует.

data Drawable = Drawable (Drawable -> SomethingSomething) {- other fields -}
draw (Drawable draw) = draw Drawable

Хотя на статическом языке это не позволяет различным объектам иметь разные атрибуты.

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

data Shape = Circle {- see second snippet -}
           | Rect {- ditto -}

draw (Circle center radius) = …
draw (Rect center height width) = …