Некоторые говорят, что если вы доведете принципы 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
}
источник
Without any additional clues
... может быть, поэтому документация является частью договора ?Ответы:
Как говорит Теластин, сравнивая статические определения функций:
в
Вы действительно ничего не потеряли, переходя от ООП к ФП.
Однако это только часть истории, поскольку функции и интерфейсы упоминаются не только в их статических определениях. Их также раздают . Итак, скажем, наш
MessageQuery
был прочитан другим фрагментом кода, аMessageProcessor
. Тогда мы имеем:Теперь мы не можем напрямую увидеть имя метода
IMessageQuery.Read
или его параметрint id
, но мы можем очень легко получить его через нашу IDE. В более общем смысле, тот факт, что мы передаемIMessageQuery
не просто какой-либо интерфейс с методом функция из int в строку, означает, что мы сохраняемid
метаданные имени параметра, связанные с этой функцией.С другой стороны, для нашей функциональной версии мы имеем:
Так что мы сохранили и потеряли? Ну, у нас все еще есть имя параметра
messageReader
, что, вероятно, делаетIMessageQuery
ненужным имя типа (эквивалентное ). Но теперь мы потеряли имя параметраid
в нашей функции.Есть два основных способа обойти это:
Во-первых, прочитав эту подпись, вы уже можете сделать довольно хорошее предположение, что будет происходить. Сохраняя функции короткими, простыми и связными и используя хорошие названия, вы значительно облегчаете интуитивное восприятие или поиск этой информации. Как только мы начнем читать саму функцию, это будет еще проще.
Во-вторых, во многих функциональных языках считается идиоматическим дизайном для создания небольших типов для переноса примитивов. В этом случае происходит обратное - вместо замены имени типа на имя параметра (
IMessageQuery
tomessageReader
) мы можем заменить имя параметра на имя типа. Например,int
может быть упакован в тип с именемId
:Теперь наша
read
подпись становится:Что так же информативно, как и раньше.
Как примечание, это также предоставляет нам некоторую защиту компилятора, которую мы имели в ООП. В то время как версия ООП гарантировала, что мы взяли
IMessageQuery
не просто старуюint -> string
функцию, а какую-то другую, здесь у нас есть аналогичная (но другая) защита, которую мы применяемId -> string
вместо какой-либо старойint -> string
.Я бы не хотел со 100% уверенностью сказать, что эти методы всегда будут такими же хорошими и информативными, как и наличие полной информации на интерфейсе, но я думаю, что из приведенных выше примеров вы можете сказать, что большую часть времени мы вероятно, может сделать такую же хорошую работу.
источник
При выполнении FP я склонен использовать более конкретные семантические типы.
Например, ваш метод для меня станет что-то вроде:
Это общается намного больше, чем стиль OO (/ Java)
ThingDoer.doThing()
стильисточник
read: MessageId -> Message
говорит тебе, чтоstring MessageReader.GetMessage(int messageId)
нет?Используйте хорошо названные функции.
IMessageQuery::Read: int -> string
просто становитсяReadMessageQuery: int -> string
или что-то подобное.Главное, на что следует обратить внимание, это то, что имена - это только контракты в самом широком смысле этого слова. Они работают только в том случае, если вы и другой программист выводите те же значения из названия и подчиняетесь им. Из-за этого вы действительно можете использовать любой имя, которое передает это подразумеваемое поведение. ОО и функциональное программирование имеют свои имена в немного разных местах и в несколько разных формах, но их функция одинакова.
Не в этом примере. Как я объяснил выше, отдельный класс / интерфейс с единственной функцией не является значительно более информативным, чем аналогично хорошо названная автономная функция.
Как только вы получаете более одной функции / поля / свойства в классе, вы можете получить больше информации о них, потому что вы можете видеть их связь. Это спорно, если это более информативно, чем автономные функции, которые имеют те же / аналогичные параметры или автономные функции, организованные пространства имен или модуля.
Лично я не думаю, что ОО значительно более информативен, даже в более сложных примерах.
источник
ReadMessageQuery id = <code to go fetch based on id>
let consumer (messageQuery : int -> string) = messageQuery 5
,id
параметр является простоint
. Я предполагаю, что один аргумент в том, что вы должны передаватьId
, а неint
. Фактически это сделало бы достойный ответ сам по себе.Я не согласен с тем, что одна функция не может иметь «семантического контракта». Рассмотрим эти законы для
foldr
:В каком смысле это не семантика или не контракт? Вам не нужно определять тип для 'foldrer', особенно потому, что
foldr
он однозначно определяется этими законами. Вы точно знаете, что он собирается делать.Если вы хотите использовать функцию какого-то типа, вы можете сделать то же самое:
Вам нужно только назвать и захватить этот тип, если вам нужен один и тот же контракт несколько раз:
Средство проверки типов не будет применять какую-либо семантику, присваиваемую типу, поэтому создание нового типа для каждого контракта является всего лишь образцом.
источник
foldr
. Подсказка: определениеfoldr
имеет два уравнения (почему?), А в приведенной выше спецификации их три.Практически все статически типизированные функциональные языки имеют способ псевдонимов базовых типов таким способом, который требует от вас явного объявления вашего семантического намерения. Некоторые из других ответов привели примеры. На практике опытные функциональные программисты нуждаются в очень веских основаниях для использования этих типов оболочек, потому что они вредят компоновке и возможности повторного использования.
Например, скажем, клиент хотел реализовать запрос сообщения, который был поддержан списком. В Haskell реализация может быть такой простой:
Использование
newtype Message = Message String
этого было бы намного менее простым, оставляя эту реализацию похожей на:Это может показаться не таким уж большим делом, но вы должны либо везде выполнять это преобразование типов , либо устанавливать пограничный слой в своем коде, где все выше,
Int -> String
тогда вы преобразуете егоId -> Message
для перехода на уровень ниже. Скажем, я хотел добавить интернационализацию, или отформатировать ее во всех заглавных буквах, или добавить контекст регистрации, или что-то еще. Все эти операции очень просто составить с помощьюInt -> String
и раздражает с помощьюId -> Message
. Дело не в том, что никогда не бывает случаев, когда увеличенные ограничения типа желательны, но раздражение должно стоить компромисса.Вы можете использовать синоним типа вместо оболочки (в Haskell
type
вместоnewtype
), что гораздо более распространено и не требует преобразований повсеместно, но не обеспечивает статических гарантий типа, как и ваша версия ООП, просто немного инкапсуляции. Обертки типов в основном используются там, где от клиента вообще не требуется манипулировать значением, просто сохраните его и передайте обратно. Например, дескриптор файла.Ничто не может помешать клиенту быть "подрывным". Вы просто создаете обручи для каждого клиента. Простой макет для обычного модульного теста часто требует странного поведения, которое не имеет смысла в производстве. Ваши интерфейсы должны быть написаны так, чтобы им было все равно, если это вообще возможно.
источник
Id
иMessage
являются простыми обертками дляInt
иString
, конвертировать между ними тривиально .