Является ли семантический контракт интерфейса (ООП) более информативным, чем сигнатура функции (ФП)?

16

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

Из статьи:

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

Если вы продолжите продвигать свой дизайн к все меньшим и меньшим интерфейсам, вы в конечном итоге получите конечный ролевый интерфейс: интерфейс с одним методом. Это часто случается со мной. Вот пример:

public interface IMessageQuery
{
    string Read(int id);
}

Если я возьму зависимость от IMessageQuery, часть неявного контракта заключается в том, что вызов Read(id)будет искать и возвращать сообщение с заданным идентификатором.

Сравните это с получением зависимости от эквивалентной функциональной сигнатуры int -> string. Без каких-либо дополнительных сигналов эта функция может быть простой ToString(). Если вы реализуете IMessageQuery.Read(int id)с ToString()меня могут обвинить вас в том , сознательно подрывной!

Итак, что могут сделать функциональные программисты, чтобы сохранить семантику хорошо названного интерфейса? Например, принято ли создавать тип записи с одним членом?

type MessageQuery = {
    Read: int -> string
}
AlexFoxGill
источник
5
Интерфейс ООП больше похож на класс типов FP , а не на отдельную функцию.
9000
2
У вас может быть слишком много хорошего, применение «строгого» провайдера для получения одного метода на интерфейс просто заходит слишком далеко.
gbjbaanb
3
@gbjbaanb на самом деле большинство моих интерфейсов имеют только один метод со многими реализациями. Чем больше вы применяете принципы SOLID, тем больше вы можете увидеть преимущества. Но это не по теме для этого вопроса
AlexFoxGill
1
@jk .: Ну, в Haskell это классы типов, в OCaml вы используете модуль или функтор, в Clojure вы используете протокол. В любом случае, вы обычно не ограничиваете свой интерфейс аналогом одной функции.
9000
2
Without any additional clues... может быть, поэтому документация является частью договора ?
SJuan76

Ответы:

8

Как говорит Теластин, сравнивая статические определения функций:

public string Read(int id) { /*...*/ }

в

let read (id:int) = //...

Вы действительно ничего не потеряли, переходя от ООП к ФП.

Однако это только часть истории, поскольку функции и интерфейсы упоминаются не только в их статических определениях. Их также раздают . Итак, скажем, наш MessageQueryбыл прочитан другим фрагментом кода, а MessageProcessor. Тогда мы имеем:

public void ProcessMessage(int messageId, IMessageQuery messageReader) { /*...*/ }

Теперь мы не можем напрямую увидеть имя метода IMessageQuery.Readили его параметр int id, но мы можем очень легко получить его через нашу IDE. В более общем смысле, тот факт, что мы передаем IMessageQueryне просто какой-либо интерфейс с методом функция из int в строку, означает, что мы сохраняем idметаданные имени параметра, связанные с этой функцией.

С другой стороны, для нашей функциональной версии мы имеем:

let read (id:int) (messageReader : int -> string) = // ...

Так что мы сохранили и потеряли? Ну, у нас все еще есть имя параметра messageReader, что, вероятно, делает IMessageQueryненужным имя типа (эквивалентное ). Но теперь мы потеряли имя параметра idв нашей функции.


Есть два основных способа обойти это:

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

  • Во-вторых, во многих функциональных языках считается идиоматическим дизайном для создания небольших типов для переноса примитивов. В этом случае происходит обратное - вместо замены имени типа на имя параметра ( IMessageQueryto messageReader) мы можем заменить имя параметра на имя типа. Например, intможет быть упакован в тип с именем Id:

    type Id = Id of int
    

    Теперь наша readподпись становится:

    let read (id:int) (messageReader : Id -> string) = // ...
    

    Что так же информативно, как и раньше.

    Как примечание, это также предоставляет нам некоторую защиту компилятора, которую мы имели в ООП. В то время как версия ООП гарантировала, что мы взяли IMessageQueryне просто старую int -> stringфункцию, а какую-то другую, здесь у нас есть аналогичная (но другая) защита, которую мы применяем Id -> stringвместо какой-либо старой int -> string.


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

Бен Ааронсон
источник
1
Это мой любимый ответ - FP поощряет использование семантических типов
AlexFoxGill
10

При выполнении FP я склонен использовать более конкретные семантические типы.

Например, ваш метод для меня станет что-то вроде:

read: MessageId -> Message

Это общается намного больше, чем стиль OO (/ Java) ThingDoer.doThing() стиль

Daenyth
источник
2
+1. Кроме того, вы можете кодировать гораздо больше свойств в систему типов в типизированных языках FP, таких как Haskell. Если бы это был тип haskell, я бы знал, что он вообще не выполняет IO (и, следовательно, вероятно, ссылается на какое-то отображение в памяти идентификаторов в сообщениях где-то). На языках ООП у меня нет этой информации - эта функция может вызывать базу данных как часть вызова.
Джек
1
Я не совсем понимаю, почему это будет общаться больше, чем стиль Java. Что read: MessageId -> Messageговорит тебе, что string MessageReader.GetMessage(int messageId)нет?
Бен Ааронсон,
@BenAaronson соотношение сигнал / шум лучше. Если это метод экземпляра OO, я понятия не имею, что он делает под капотом, он может иметь какую-либо зависимость, которая не выражена. Как упоминает Джек, когда у тебя более сильные типы, у тебя больше гарантий.
Daenyth
9

Итак, что могут сделать функциональные программисты, чтобы сохранить семантику хорошо названного интерфейса?

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

IMessageQuery::Read: int -> stringпросто становится ReadMessageQuery: int -> stringили что-то подобное.

Главное, на что следует обратить внимание, это то, что имена - это только контракты в самом широком смысле этого слова. Они работают только в том случае, если вы и другой программист выводите те же значения из названия и подчиняетесь им. Из-за этого вы действительно можете использовать любой имя, которое передает это подразумеваемое поведение. ОО и функциональное программирование имеют свои имена в немного разных местах и ​​в несколько разных формах, но их функция одинакова.

Является ли семантический контракт интерфейса (ООП) более информативным, чем сигнатура функции (ФП)?

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

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

Лично я не думаю, что ОО значительно более информативен, даже в более сложных примерах.

Telastyn
источник
2
Как насчет имен параметров?
Бен Ааронсон
@BenAaronson - как насчет них? Как в ОО, так и в функциональных языках можно указывать имена параметров, так что это толчок. Я просто использую сокращение подписи типа здесь, чтобы соответствовать вопросу. На реальном языке это выглядело бы примерно такReadMessageQuery id = <code to go fetch based on id>
Telastyn
1
Хорошо, если функция принимает интерфейс в качестве параметра, а затем вызывает метод для этого интерфейса, имена параметров для метода интерфейса легко доступны. Если функция принимает в качестве параметра другую функцию, нелегко назначить имена параметров для этой функции
Бен Ааронсон,
1
Да, @BenAaronson - это одна из частей информации, которая теряется в сигнатуре функции. Например let consumer (messageQuery : int -> string) = messageQuery 5, idпараметр является просто int. Я предполагаю, что один аргумент в том, что вы должны передавать Id, а не int. Фактически это сделало бы достойный ответ сам по себе.
AlexFoxGill
@AlexFoxGill На самом деле я только что писал что-то в том же духе!
Бен Ааронсон,
0

Я не согласен с тем, что одна функция не может иметь «семантического контракта». Рассмотрим эти законы для foldr:

foldr f z nil = z
foldr f z (singleton x) = f x z
foldr f z (xn <> ys) = foldr f (foldr f z ys) xn

В каком смысле это не семантика или не контракт? Вам не нужно определять тип для 'foldrer', особенно потому, чтоfoldr он однозначно определяется этими законами. Вы точно знаете, что он собирается делать.

Если вы хотите использовать функцию какого-то типа, вы можете сделать то же самое:

-- The first argument `f` must satisfy for all x, y, z
-- > f x x = true
-- > f x y = true and f y x = true implies x = y
-- > f x y = true and f y z = true implies f x z = true
sort :: forall 'a. (a -> a -> bool.t) -> list.t a -> list.t a;

Вам нужно только назвать и захватить этот тип, если вам нужен один и тот же контракт несколько раз:

-- Type of functions `f` satisfying, for all x, y, z
-- > f x x = true
-- > f x y = true and f y x = true implies x = y
-- > f x y = true and f y z = true implies f x z = true
type comparison.t 'a = a -> a -> bool.t;

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

Джонатан Каст
источник
1
Я не думаю, что ваш первый пункт касается вопроса - это определение функции, а не подпись. Конечно, взглянув на реализацию класса или функции, вы можете сказать, что они делают - возникает вопрос, можете ли вы сохранить семантику на уровне абстракции (сигнатура интерфейса или функции)
AlexFoxGill
@AlexFoxGill - это не определение функции, хотя оно однозначно определяет foldr. Подсказка: определение foldrимеет два уравнения (почему?), А в приведенной выше спецификации их три.
Джонатан В ролях
Я имею в виду, что foldr - это функция, а не сигнатура функции. Законы или определения не являются частью подписи
AlexFoxGill
-1

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

Например, скажем, клиент хотел реализовать запрос сообщения, который был поддержан списком. В Haskell реализация может быть такой простой:

messages = ["Message 0", "Message 1", "Message 2"]
messageQuery = (messages !!)

Использование newtype Message = Message Stringэтого было бы намного менее простым, оставляя эту реализацию похожей на:

messages = map Message ["Message 0", "Message 1", "Message 2"]
messageQuery (Id index) = messages !! index

Это может показаться не таким уж большим делом, но вы должны либо везде выполнять это преобразование типов , либо устанавливать пограничный слой в своем коде, где все выше, Int -> Stringтогда вы преобразуете его Id -> Messageдля перехода на уровень ниже. Скажем, я хотел добавить интернационализацию, или отформатировать ее во всех заглавных буквах, или добавить контекст регистрации, или что-то еще. Все эти операции очень просто составить с помощью Int -> Stringи раздражает с помощью Id -> Message. Дело не в том, что никогда не бывает случаев, когда увеличенные ограничения типа желательны, но раздражение должно стоить компромисса.

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

Ничто не может помешать клиенту быть "подрывным". Вы просто создаете обручи для каждого клиента. Простой макет для обычного модульного теста часто требует странного поведения, которое не имеет смысла в производстве. Ваши интерфейсы должны быть написаны так, чтобы им было все равно, если это вообще возможно.

Карл Билефельдт
источник
1
Это больше похоже на комментарий к ответам Бена / Денита - что вы можете использовать семантические типы, но не из-за неудобств? Я не отрицал вас, но это не ответ на этот вопрос.
AlexFoxGill
-1 Это никак не повредит компоновке или повторному использованию. Если Idи Messageявляются простыми обертками для Intи String, конвертировать между ними тривиально .
Майкл Шоу
Да, это тривиально, но все равно раздражает делать повсюду. Я добавил пример и немного описал разницу между использованием синонимов типов и оболочек.
Карл Билефельдт
Это похоже на размер вещи. В небольших проектах такие типы упаковщиков, вероятно, не так уж и полезны. В больших проектах естественно, что границы будут составлять небольшую часть всего кода, поэтому выполнение этих типов преобразований на границе, а затем передача обернутых типов повсюду не является слишком сложной. Также, как сказал Алекс, я не понимаю, как это ответ на вопрос.
Бен Ааронсон
Я объяснил, как это сделать, и почему функциональные программисты этого не делают. Это ответ, даже если это не тот человек, который хотел. Это не имеет ничего общего с размером. Идея о том, что намеренно усложнять повторное использование кода каким-то образом полезно, является концепцией ООП. Создание типа продукта, который добавляет ценность? Все время. Создание типа в точности как широко используемый тип, за исключением того, что вы можете использовать его только в одной библиотеке? Практически неслыханно, за исключением, возможно, кода FP, который интенсивно взаимодействует с кодом ООП, или написанного кем-то, в основном знакомым с идиомами ООП.
Карл Билефельдт