апликативное программирование 2
Feb. 4th, 2012 09:53 pm![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
В этой части мы ещё тоже не дойдём до апликативного стиля - но обсудим альтернативы и затронем монады.
Чтобы справиться с нетотальностью функций, с которыми мы имеет дело, есть несколько альтернатив.
Рассмотрим очевидные:
-
- exceptions
- null object
-
В принципе, джава унаследовала
То есть, вернуть
И люди вполне спокойно пишут функции, которые проверяют параметр на
Этот же код можно написать более прилично (10x
sab123):
Ниоткуда не известно, что функция, получающая
А забыли один раз - и наш
Но в принципе, если нам попался
Если б мы были циниками, а не серьёзным джава-программистами, озабоченными производительностью компьютера, наносекундами и килобайтами, мы б могли писать так:
Согласитесь, выглядит не очень прилично - но гораздо более осмысленно. К тому же, вполне монадично - если
Тут только такая проблема, что мы не знаем, откуда прибежал этот
Хоть и выглядит дико, но у нас тут проступают черты вполне логичного приёмчика - вычисления делаются только в случае, если данные имеются, а исключительный случай отделяется от нормального там, где мы "вылазим из монады".
Истинный джавщик не может без паттерна. Null Object горячо рекомендуют наши кумиры, Нил Гафтер и Джош Блок. Имеет смысл - если ничего не нашли в базе, возвращаем не страшный null, а его представителя в данном типе;
Смысл этой операции такой, что теперь в коде мы можем предполагать тотально определённые функции и не отвлекаться на исключения.
Функция
вернёт пустую строку.
В сущности что мы сделали - это каждый тип пополнили своим отдельным
4.
Ну или можно сделать умственное усилие и добавить интерфейс
Она монада потому, что имеются две стандартные операции:
единица монады, строящая по значению
монадное умножение, часто называемое операцией flatten - если есть
Монады бывают разной степени сложности,
Сравните с плющеньем списка:
На Скале ту же операцию можно выразить проще:
Ну или ещё проще:
С помощью
Чтобы можно было писать цикл, нужно вот что:
На скале такая конструкция записывается в виде одного цикла:
За сценой такой хитрый цикл разворачивается в последовательность сплющиваний.
Нет, на самом деле надо добавить ещё одну операцию, преобразование,
Если у нас есть функция
На скале это выглядит практически так же:
Или просто
Думаю, Вы можете написать аналогичную операцию для монады списка (
Монады помогают примирить реальный мир с его, э, исключениями и идеальный мир функций, который мы, теоретически, любим в компьютерах.
К сожалению, и с монадами нас подстерегают неприятности, о которых я расскажу в следующей части. Но не думайте, что без монад всё здорово и можно обойтись if-ами. Напротив; с if-ами вы просто не замечаете проблем в вашем коде - они парят как коршуны где-то над головами, а мы как мыши суетимся, ищем, где бы тут соптимизировать да все случаи учесть. "Книги про паттерны" не помогают.
В этой части мы ещё тоже не дойдём до апликативного стиля - но обсудим альтернативы и затронем монады.
Чтобы справиться с нетотальностью функций, с которыми мы имеет дело, есть несколько альтернатив.
Рассмотрим очевидные:
-
null
- exceptions
- null object
-
Option
1. null
В принципе, джава унаследовала
null
из си, и жила не тужила, пока не появились ещё дженерики, и стало как-то неудобно. До монад далеко, но уже неудобно.То есть, вернуть
null
- да никаких проблем, кто только не возвращает null
; и принять null
в качестве параметра тоже милое дело, дескать, нетути этого вашего параметра, ну не шмогла я.И люди вполне спокойно пишут функции, которые проверяют параметр на
null
и если что, возвращают null
; чем не Option
. Да тем, что всё это нужно явно прописывать. String countryName(User user) { if (user == null) return null; String phone = user.getPhone(); if (phone == null) return null; String countryCode = PSTN.extractCountryCode(phone); if (countryCode == null) return null; Country country = Countries.findByCode(countryCode); if (country == null) return null; return country.getName();
Этот же код можно написать более прилично (10x
![[livejournal.com profile]](https://www.dreamwidth.org/img/external/lj-userinfo.gif)
String countryName(String userId) { User user; Phone phone; String cc; Country country; return userid != null && (user = db.findUser(userid)) != null && (phone = user.getPhone) != null && (cc = phone.getCountryCode) != null && (country = Countries.findByCode(cc)) != null ? country.getName() : null }
Ниоткуда не известно, что функция, получающая
null
, волшебным образом должна вернуть null
, или что метод, вызванный на объекте null
, вернёт null
, а не, к примеру, 0, если хотим длину. Поэтому и пишут методы isEmpty(String s) { return s == null ? 0 : s.isEmpty(); }
.А забыли один раз - и наш
null
куда-то протёк внутрь, в какую-нибудь библиотечную функцию, та дальше передала - и потом разбирайся, в чём вообще дело. В реальности-то в джаве принято возвращать null
на выходе, но не проверять на входе.Но в принципе, если нам попался
null
вместо осмысленного значения, и нам надо с него что-то взять, то мы получим NPE
; исключение - более-менее монада, в отличие от null
.2. exceptions
Если б мы были циниками, а не серьёзным джава-программистами, озабоченными производительностью компьютера, наносекундами и килобайтами, мы б могли писать так:
String countryName(User user) { try { return Countries.findByCode(PSTN.extractCountryCode(user.getPhone())); } catch (NPE npe) { return null; } }
Согласитесь, выглядит не очень прилично - но гораздо более осмысленно. К тому же, вполне монадично - если
NPE
на нас не бросится, то вернём "данное", ну а если бросится, так только одно. Может быть, так и надо на джаве писать. Тут только такая проблема, что мы не знаем, откуда прибежал этот
NPE
- может, вовсе и не из-за того, что у нас данных нету; для прототипа такой код годится, а в реальной программе вряд ли. Но для прототипа годится.Хоть и выглядит дико, но у нас тут проступают черты вполне логичного приёмчика - вычисления делаются только в случае, если данные имеются, а исключительный случай отделяется от нормального там, где мы "вылазим из монады".
3. Null Object Pattern
Истинный джавщик не может без паттерна. Null Object горячо рекомендуют наши кумиры, Нил Гафтер и Джош Блок. Имеет смысл - если ничего не нашли в базе, возвращаем не страшный null, а его представителя в данном типе;
NullUser, у которого есть
NullPhoneу которого есть пустой номер (""), по которому PSTN возвращает пустой код страны (или код пустой страны... хм, я знаю одну пустую страну, называется Атолл Диего Гарсиа - код страны есть, но нет ни одного опубликованного телефона; все на кораблях. Но мы введём пустую страну, NullCountry, и тогда Всё Будет Правильно.
Смысл этой операции такой, что теперь в коде мы можем предполагать тотально определённые функции и не отвлекаться на исключения.
Функция
String countryName(User user) { return Countries.findByCode(PSTN.extractCountryCode(user.getPhone())); }
вернёт пустую строку.
В сущности что мы сделали - это каждый тип пополнили своим отдельным
None/NAN/Not_a_Country/Not_a_Phone/Not_a_Pipe
. Практически монада Option, но имплементированная для каждого типа отдельно, вручную; для различения ещё нужно добавить метод boolean isValid()
, который будет возвращать true
для нормальных значений и false
для нашего None.4. Option
Ну или можно сделать умственное усилие и добавить интерфейс
Option<T>
, у которого будет две имплементации, None
и Some(T value)
.Она монада потому, что имеются две стандартные операции:
единица монады, строящая по значению
T x
значение new Some<T>(x)
- т.е. просто конструктор, имонадное умножение, часто называемое операцией flatten - если есть
Option<Option<T>>
, то из него получается Option<T>
. Монады бывают разной степени сложности,
Option
из них самая простая. Её и плющить особенно легко: None
превращается в None
, Some(None)
превращается в None
, Some(Some(x))
превращается в Some(x)
.Сравните с плющеньем списка:
List<List<T>> → List<T>
- в данном случае мы пробегаем по списку списков и возвращаем список всего, что нам попалось по дороге:<T> List<T> flatten(List<List<T>> lol) { List<T> result = new // something for (List<T> list : lol) { for (T element : list) { result.add(element); } } return result; }
На Скале ту же операцию можно выразить проще:
def flatten[T] (lol: List[List[T]]): List[T] = { val result = new scala.collection.mutable.// какой-нибудь список подходящего типа for (list <- lol element <- list) result.add(element) result }
Ну или ещё проще:
def flatten[T] (lol: List[List[T]]) = lol.flatten
С помощью
Option
с частичными функциями обращаться проще - вместо списка, вместо null
, вместо исключения мы просто возвращаем всегда Option
; а чтобы применить функцию к "содержимому Option
", мы можем использовать цикл:for (User user : db.findUser(userid)) { for (Phone phone : user.getPhone()) { for (String countryCode: phone.getCountryCode()) { for (Country country: Countries.findByCode(countryCode)) { System.out.println("User " + user + " is from " + country); }}}}
Чтобы можно было писать цикл, нужно вот что:
Option<T> implements Iterable<T>
- ну это несложно. None всегда возвращает пустой Iterable
, а Some
возвращает одноэлементный Iterable
.На скале такая конструкция записывается в виде одного цикла:
for (user <- db.findUser(userid) phone <- user.getPhone cc <- phone.getCountryCode country <- Countries.findByCode(cc) ) println("User " + user + " is from " + country)
За сценой такой хитрый цикл разворачивается в последовательность сплющиваний.
Нет, на самом деле надо добавить ещё одну операцию, преобразование,
fmap
Если у нас есть функция
<A,B> B f(A a) {...}
, то монада... ну, скажем, та же Option
, определяет функцию <A, B> Option<B> Option_f(Option<A> aOpt) { for (a : aOpt) { return new Some<B>(f(a)); } return None; }
На скале это выглядит практически так же:
def Option_f[A,B](aOpt: Option[A]) = { for (a <- aOpt) return Some(f(a)) None }
Или просто
def Option_f[A,B](aOpt: Option[A]) = aOpt map f
Думаю, Вы можете написать аналогичную операцию для монады списка (
List
).Монады помогают примирить реальный мир с его, э, исключениями и идеальный мир функций, который мы, теоретически, любим в компьютерах.
К сожалению, и с монадами нас подстерегают неприятности, о которых я расскажу в следующей части. Но не думайте, что без монад всё здорово и можно обойтись if-ами. Напротив; с if-ами вы просто не замечаете проблем в вашем коде - они парят как коршуны где-то над головами, а мы как мыши суетимся, ищем, где бы тут соптимизировать да все случаи учесть. "Книги про паттерны" не помогают.
no subject
Date: 2012-02-05 06:03 am (UTC)no subject
Date: 2012-02-05 06:24 am (UTC)no subject
Date: 2012-02-05 07:01 am (UTC)no subject
Date: 2012-02-05 07:52 am (UTC)Я не силен в джаве, но разве нельзя сделать как-то так:
Ну, надо написать еще пачку исключений, да. Зато, когда все сломается, будет понятно где, почему, и что с этим делать. Даже необработанное исключение, если выскочит в готовой программе, принесет пользу, потому что пользователь будет звонить/писать в поддержку не "у меня все сломалось", а "страну не находит".
no subject
Date: 2012-02-05 08:17 am (UTC)no subject
Date: 2012-02-05 08:52 am (UTC)А представьте, что наша задача сейчас такова: вывести на экран информацию о пользователе, которая содержит как обязательные, так и необязательные поля. Например, имя пользователя -- обязательное полe, a телефонный номер и код страны в нем -- необязательные. Если необязательное поле не заполнено, это не считается ошибкой, и мы просто игнорируем его при выводе.
Тогда Option[T] как раз подходит для таких необязательных полей.
А случай, когда не заполнено обязательное поле, т.е. случилась ошибка, можно рассмотреть отдельно в следующем посте.
no subject
Date: 2012-02-05 09:32 am (UTC)На самом деле, я наверное придираюсь к примеру. По сути-то все правильно. Option тут действительно позволяет избавиться от исключений.
no subject
Date: 2012-02-05 10:53 am (UTC)1) пользователя нет в базе данных
2) у него нет телефона
3) у телефона нет номера страны
4) страна по номеру не найдена.
Похоже, что Option вполне подходит для (2) и (3), а для (1) и (4) -- не вполне.
no subject
Date: 2012-02-05 05:39 pm (UTC)no subject
Date: 2012-02-05 08:08 am (UTC)Э-э-э... Так же:
no subject
Date: 2012-02-05 05:41 pm (UTC)no subject
Date: 2012-02-05 09:12 am (UTC)То есть, сначала мы ввели Optiоn[T] с очевидным методом isValid() безо всяких map, flatten, и flatMap. Теперь запишем предыдущий пример (с проверками на null), по-новому, то есть используя isValid. Уже получше ? А теперь добавляем в Optiоn методы map и flatten и переписываем пример снова. И наконец, заменяем map и flatten на flatMap.
(Тут, наверное, интересно будет показать, что map, flatten на flatMap определены и для коллекций, и это сходство неслучайно :) Потому что на самом деле это монадные операции).
Возвращаясь к нашему примеру, видим, что получилось то, что мы хотели. Ну а для компактности переписываем наш пример еще раз с использованием for-comprehension.
***
Кстати говоря, обучить использованию Option, в общем, несложно. Дальше сложнее. Надеяться, что программисты, обучившись Option, дальше сами смогут использовать, например, Either, нельзя. Нужно учить и об'яснять дальше
под угрозой лишения квартальной премии. На следующем занятии можно усложнить задачу. Теперь, если обязательное поле отсутствует, нужно вывести сообщение об ошибке (например, "отсутствует имя пользователя").no subject
Date: 2012-02-05 05:42 pm (UTC)no subject
Date: 2012-02-08 09:11 am (UTC)Вряд ли в UC Riverside есть курсы по этому делу...
no subject
Date: 2012-02-05 12:18 pm (UTC)no subject
Date: 2012-02-05 07:51 pm (UTC)Только есть еще аннотации @NotNull/@Nullable и Preconditions, которые сработают и без ключика -ea в production.
Но в целом, пост Влада понятен. И мне кажется он несколько о другом. То есть, очевидно, существуют ситуации, когда параметр не обязателен. В посте предлагается способен подумать над тем, как можно обрабатывать такие ситуации на ином уровне абстракции, более обобщенном и таким образом ввести фактически новую идиому в язык, если говорить принятыми терминами.
no subject
Date: 2012-02-05 07:54 pm (UTC)no subject
Date: 2012-02-05 08:05 pm (UTC)Но от проверок не уйти, просто они будут в другом месте.
no subject
Date: 2012-02-05 01:13 pm (UTC)if-ами обойтись можно. Трудность в том, чтобы if-ы _везде_ расставить одинаково правильно. Монада позволяет такой if поставить один раз - в определении монады. Вот так и минус один источник ошибок.
no subject
Date: 2012-02-05 01:33 pm (UTC)Вообще, мне монадный for в скале давно нравится. Особенно в плане с getPhone на getPhones переводить в случае чего. :)
Кстати, а где сейчас наиболее красивый скальный код пишут (не Scalaz, а что-то более земное)?
no subject
Date: 2012-02-05 05:43 pm (UTC)no subject
Date: 2012-06-25 10:17 am (UTC)no subject
Date: 2012-06-25 04:40 pm (UTC)no subject
Date: 2012-06-25 07:05 pm (UTC)no subject
Date: 2012-06-25 07:42 pm (UTC)no subject
Date: 2012-02-05 04:34 pm (UTC)no subject
Date: 2012-02-05 08:01 pm (UTC)Здесь есть очевидные плюсы, но есть очевидные и минусы. Например, ведь довольно очевидно(?), что хотя мы можем все привести к списку, но на самом-то деле, по-сути не все является списоком или является списоком лишь отчасти, искуственно, отражаем природу той или иной сущности. То есть придеться отойти от ООП, как наиболее(?) естественной формы выражения сущностей и полностью перестроить свое восприятие.
То есть разница между моделью предметной области и кодом будет огромной. В принципе и ООП с его паттернами, тоже далек, но в нем существует хотя бы четкая доменная модель. А как здесь быть?
Ну и вот я слышал пару раз про DDD, который как раз эту проблему пытается решить, но применить не довелось.
no subject
Date: 2012-02-06 07:14 am (UTC)no subject
Date: 2012-02-05 10:27 pm (UTC)no subject
Date: 2012-02-06 12:42 am (UTC)no subject
Date: 2012-02-06 12:53 am (UTC)Вообще говоря, вся проблема выглядит надуманной. Уж если на то пошло, чем оно лучше, чем NullObject или даже простой null? Вот, пожалуйста:
no subject
Date: 2012-02-06 03:13 am (UTC)no subject
Date: 2012-02-06 07:19 am (UTC)no subject
Date: 2012-02-06 07:23 am (UTC)no subject
Date: 2012-02-06 06:10 pm (UTC)no subject
Date: 2012-02-06 06:19 pm (UTC)no subject
Date: 2012-02-06 06:55 pm (UTC)no subject
Date: 2012-02-06 07:29 pm (UTC)no subject
Date: 2012-02-06 09:15 pm (UTC)ок, чтоб два раза не вставать:
1. всё переводится в if-ы, т.к. это атомарная единица логики. Так что, это достаточное условие. Что не доказано, так это необходимость.
2. Сравните с ситуацией, когда бы null заменял пустые списки. Пустой список как объект позволяет вызывать на нём методы (=задача архитектора задать кошерную реализацию оных). Пустой список как null требует реализовать эти методы везде (=задача пользователя, плюс проблема с reuse).
no subject
Date: 2012-02-06 09:51 pm (UTC)2. Но в данном случае мы не вызываем никаких методов до проверки. Однако даже в вашем случае, ну, допустим, позволяет. Вот мы имеем: List herd = foo.getHerd(); herd.first().moo(); Допустим, нам не нравится, что метод getHerd() возвращает null в случае, когда нужного стада нет, потому что first() не работает. Вот мы вернули пустой список, и first() заработал. Чем лучше стало? moo() то всё равно вызывать не на чем. Списки - это сервисные структуры, у них есть клиенты, и эти клиенты всё равно вынуждены будут как-то прописывать случай пустого списка. Какая в таком случае разница между null и пустым списком?
no subject
Date: 2012-02-06 11:28 pm (UTC)2. списки - это сервисные структуры. Поэтому никому и в голову не придёт пустые списки реализовать как null.
no subject
Date: 2012-02-06 11:48 pm (UTC)2. Почему не придёт? Если это так очевидно - наверное, это должно быть очень легко обьяснить? Буквально в двух словах? Не в чём разница между [] и null, а почему в случае getHerd() надо возвращать именно [], а не null? Причём настолько надо, что никому и в голосу не придёт делать по-другому?
no subject
Date: 2012-02-07 08:48 am (UTC)2. потому что если не пользоваться Option, тип результата A|Nothing. Значит, везде, даже где используется просто A, должен быть паттерн матчинг. Если вместо типа A|Nothing используем список [A], то нужно проверять пустоту списка (тоже типа паттерн матчинг) _не везде_, а там, где используется Nothing. В остальных случаях пустой список себя ведёт по-другому, поэтому можно положиться на его поведение, на реализацию его итератора.
Разница маленькая. Примерно как вместо наследования и перегрузки операций пользоваться instanceof. Да, можно. В C так до сих пор делают.
no subject
Date: 2012-02-06 09:24 am (UTC)Кстати, а как идиомой 'for/Iterable' заматчить None?
no subject
Date: 2012-02-06 03:15 pm (UTC)no subject
Date: 2012-02-06 04:48 pm (UTC)no subject
Date: 2012-02-10 01:41 am (UTC)Да!
Спасибо.
Ждем продолжения.