Feb. 4th, 2012

juan_gandhi: (Default)
1, 2, 3, 4, 5, 6, 7, 8, 9, 10


В этой части мы ещё тоже не дойдём до апликативного стиля - но обсудим альтернативы и затронем монады.

Чтобы справиться с нетотальностью функций, с которыми мы имеет дело, есть несколько альтернатив.

Рассмотрим очевидные:
- 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] sab123):
  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-ами вы просто не замечаете проблем в вашем коде - они парят как коршуны где-то над головами, а мы как мыши суетимся, ищем, где бы тут соптимизировать да все случаи учесть. "Книги про паттерны" не помогают.

Profile

juan_gandhi: (Default)
Juan-Carlos Gandhi

August 2025

S M T W T F S
      12
3456789
10 11 12 13141516
17181920212223
24252627282930
31      

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Aug. 16th, 2025 03:11 am
Powered by Dreamwidth Studios