Feb. 4th, 2012
апликативное программирование 2
Feb. 4th, 2012 09:53 pm1, 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-ами вы просто не замечаете проблем в вашем коде - они парят как коршуны где-то над головами, а мы как мыши суетимся, ищем, где бы тут соптимизировать да все случаи учесть. "Книги про паттерны" не помогают.