вторник, 17 января 2012 г.

[prog] Прочитал тут статью Аарона Минского “OCaml for the Masses”…

которая на acmqueue с подзаголовком “Why the next language you learn should be functional” (статья не новая, от 27 сентября 2011, но мне попалась на глаза случайно пару дней назад).

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

Развитие темы под катом, поскольку букв много.

Первый пример там стандартный, связанный с неким простеньким интерпретатором выражений, который на OCaml выглядит очень кратко:

type 'a expr = | True 
               | False 
               | And  of  'a expr * 'a  expr 
               | Or   of  'a expr * 'a  expr 
               | Not  of  'a expr 
               | Base of  'a  
 
let  rec eval eval_base expr  = 
   let  eval' x = eval eval_base x in 
   match expr with 
   | True  -> true 
   | False -> false 
   | Base base  -> eval_base base 
   | And  (x,y) -> eval' x && eval' y  
   | Or  (x,y)  -> eval' x || eval' y 
   | Not  x     -> not (eval' x) 

А на Java намного, сильно намного многословнее:

public  abstract class Expr<T> { 
 
  public interface Evaluator<T> { boolean evaluate(T value); } 
  public abstract boolean eval(Evaluator<T> evaluator); 
 
  public class True<T> extends Expr<T> { 
    public boolean eval(Evaluator<T> evaluator) { return true; } 
  } 
  public class False<T> extends Expr<T> { 
    public boolean eval(Evaluator<T> evaluator) { return false; } 
  } 
  public class Base<T> extends Expr<T> { 
    public final T value; 
    public Base(T value) { this.value = value; } 
    public boolean eval(Evaluator<T> evaluator) 
    { return evaluator.evaluate(value); } 
  } 
  public class And<T> extends Expr<T> { 
    public final Expr<T> expr1; 
    public final Expr<T> expr2; 
    public And(Expr<T> expr1, Expr<T> expr2)  { 
      this.expr1 = expr1; 
      this.expr2 = expr2; 
     } 
    public boolean eval(Evaluator<T> evaluator) { 
      return expr1.eval(evaluator) && expr2.eval(evaluator); 
     } 
  } 
  public class Or<T> extends Expr<T> { 
    public final Expr<T> expr1; 
    public final Expr<T> expr2; 
    public Or(Expr<T> expr1, Expr<T> expr2)  { 
      this.expr1 = expr1; 
      this.expr2 = expr2; 
     } 
    public boolean eval(Evaluator<T> evaluator) { 
      return expr1.eval(evaluator) || expr2.eval(evaluator); 
     } 
  } 
  public class Not<T> extends Expr<T> { 
    public final Expr<T> expr; 
    public Not(Expr<T> expr) { this.expr = expr; } 
    public boolean eval(Evaluator<T> evaluator) 
     { return !expr.eval(evaluator); } 
  } 

Тут да, тут не поспоришь. Преимущества OCaml-а очевидны. Поэтому-то такой пример и любят приводить. Иногда даже складывается впечатление, что на ФЯ ничего кроме разбора грамматик и не делают ;)

В качестве небольшой ремарки к данному примеру приведу всего лишь несколько реплик. Как давно кому-нибудь из читателей этой заметки приходилось иметь дело со столь простыми выражениями? Лично я уже и не помню. Несколько раз за всю карьеру приходилось интерпретаторы (много посложнее приведенных в примере), но тогда хватало ума использовать уже готовые генераторы парсеров, поэтому с подобными страшилками и не сталкивался.

В одном же из случаев пришлось что-то вроде императивных конструкций на синтаксис конфигурационного файла натягивать. Тогда у меня на С++ получилось что-то аналогичное Java-примеру. Но и там количество прикладного кода внутри каждого из классов было настолько много, что “синтаксический оверхэд” был сосем не велик. Так что на игрушечном примере все выглядит страшно. Вспоминая же, как оно бывает на практике – уже совсем не так ;)

Следующий пример должен был показать силу системы типов в поиске ошибок. Был приведен пример функции, удаляющей повторно идущие одинаковые элементы, сначала на Python:

# Removes sequential duplicates, e.g.,
# destutter([1,1,4,3,3,2])  = [1,4,3,2]
def destutter(list): 
    l = [] 
    for i in range(len(list)): 
        if list[i] != list[i+1]: 
             l.append(list[i]) 
    return

затем на OCaml:

let rec destutter l = 
  match l with 
  | []             -> [] 
  | x :: y :: rest -> 
    if  x = y then destutter (y :: rest) 
    else  x :: destutter (y :: rest) 

Оба первых варианта кода содержат одинаковую ошибку – нет корректной обработки ситуации, когда в исходном списке всего один элемент. Только для OCaml-а ошибку отыскивает компилятор, т.к. он понимает, что паттерн-матчинг в destutter не содержит всех возможных вариантов. А вот для Python-овского варианта нужно делать тесты.

ИМХО, такой пример скорее вреден, чем полезен. Я считаю, что разработчик должен писать код понимая, что он хочет, и что должно быть получено в итоге. А раз так, то ситуация со списком из одного элемента для разработчика должна быть даже более интересна, чем ситуация с пустым списком. Ведь именно на таком списке i-й элемент есть, а его сравнение с (i+1)-м элементом невозможно. Но разработчик поведением функции в этом случае не озаботился, т.е. проявил свою некомпетентность. Тем не менее, в варианте с OCaml-ом ему это сошло с рук.

Да, я понимаю, что сильно придираюсь, и что все, даже самые компетентные (включая меня самого) ошибаются в тривиальных ситуациях. Но если речь идет о гипотетических тривиальных ошибках, то где гарантия, что OCaml будет их все ловить? Нет такой гарантии. А посему этот пример идет лесом, т.к. если гарантий нет, то все равно нужно будет делать тесты и тогда какая хрен разница, на Python-е написан код или на OCaml?

Ну и последний пример. Который, якобы, более приближен к жизни, чем предыдущие. Вроде того, что есть некое TCP-соединение и нужно хранить в программе его описание. Первый вариант такой:

type connection_state = 
| Connecting 
| Connected 
| Disconnected 
 
type connection_info = { 
  state:                   connection_state; 
  server:                  inet_addr; 
  last_ping_time:          time option; 
  last_ping_id:            int    option; 
  session_id:              string option; 
  when_initiated:          time option; 
  when_disconnected: time  option; 
} 

а второй, использующий возможности и лаконичность OCaml-а, такой:

type connecting   = { when_initiated:  time; } 
type connected    = { last_ping  : (time * int) option; 
                                 session_id: string; } 
type disconnected = { when_disconnected: time;  } 
 
type connection_state = 
| Connecting   of connecting 
| Connected    of connected 
| Disconnected of disconnected 
 
type connection_info = { 
   state:  connection_state; 
   server: inet_addr; 
} 

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

Во-первых, от лаконичности описаний не остается и следа (на каком бы языке программирования оно не было сделано) – на каждую строчку декларации будет приходиться по 5-10-15-и-более строк с комментариями (да еще со специальными тегами вроде @author, @version, @note, @attention и пр.).

Во-вторых, может статься так, что состояний потребуется больше. И что часть информации между ними должна будет разделяться. Например, состояние Connected указывает, что установлено физическое соединение, но еще не начата логическая сессия. А для логической сессии нужно будет ввести состояние SessionOpened. Причем и для Connected, и для SessionOpened нужен будет общий счетчик пингов (например, такая ситуация есть в протоколе SMPP). Вот вновь и вылезут те самые инварианты, от поддержки которых хотелось избавиться. И простая линейная структура connection_state вновь может стать более удобной.

В-третьих, если уж противопоставлять OCaml и функциональщину объектно-ориентированному виду, то почему бы на той же Java не выделить понятие StateHandler-а как аналога connection_state, а в его наследники поместить не только специфическую для состояния информацию, но еще и код по обработке соединения в этом состоянии? Еще большой вопрос что именно с течением времени окажется проще развивать и сопровождать.

В общем, примеры меня ни в чем не убедили. И это главный недостаток статьи. Не языка, о котором идет речь. Поскольку как раз об OCaml-е у меня остались вполне нормальные впечатления – как раз один из тех ФЯ, которые легко осваиваются такими замшелыми императивщиками, как я.

Да и вообще не понятно, зачем статья была написана. Ну есть контора JaneStreet, которая начинала свой ключевой софт в виде VBA-скриптов для Excel, затем пыталась переписать это на Java, потом на C# и OCaml. Ну пишут там сейчас 65 сотрудников на OCaml. Ну написано там за все это время 2 миллиона строк на OCaml (кстати не понятно, почему так мало). Это все понятно. К чему статью-то было писать? Если для JaneStreet OCaml – это конкурентное преимущество, то лучше было молчать в тряпочку и никому не признаваться ;)

Так что статья не понравилась. Ничего нового в ней для себя не нашел. И желание заняться OCaml-ом после нее не возникло. Так что рекомендовать к прочтению не могу.

А вот познакомиться с OCaml-ом имеет смысл. Хотя бы для того, чтобы подружиться с ML-ным синтаксисом :) Тем более, что OCaml – язык живой и даже вполне себе кроссплатформенный.

Отправить комментарий