понедельник, 9 марта 2009 г.

Об интуитивности императивного и функционального программирования

Данная тема навеяна очередным RSDN-новским флеймом по поводу функционального программирования (ФП). Толчком стало утверждение, что в 1980-е годы объектно-ориентированное программирование (ООП) с трудом завоевывало себе место под солнцем. Не знаю, что происходило на Западе в 1980-е, но в 1992-м я уже программировал на C++ с объектами, даже не подозревая, что использую ООП. Об этом я узнал несколько позже, наверное, в 1994-м. Тогда же, может чуть раньше, я стал понимать, что переход к ООП действительно требует некоторого изменения способа мышления. Но у меня самого это изменение прошло незаметно и безболезненно. Чего не происходит по отношению к ФП. Почему же?

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

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

Со временем человек учится воздействовать на окружающий мир не только через действия, но и через указания. Начиная с детских требования “хочу конфету”, заканчивая управлением подчиненными в зрелом возрасте. Мы понимаем, что нужно отдавать команды, которые будут приводить к результату. Команды должны быть упорядочены и осмысленны, а это уже программирование. И, что еще важно, команды используются чтобы изменять окружающий нас мир.

Так же в реальной жизни мы встречаемся с модульностью и структурным подходом. Еще в детском саду мы узнаем, что в группе самая главная - воспитательница, но она не самая главная в саду, есть еще и заведующая. Потом мы видим иерархии в школе, в спорте, в армии, на производстве…

Итак, все наше существование показывает нам, что мир вокруг изменяем и, местами, структурирован.

Когда начинается процесс обучения программированию, мы видим то же самое. Первые программы, которые я писал в школе, были предназначены для того, чтобы производить видимые изменения - рисунки на экране. Было здорово - я давал команды компьютеру, чтобы он изменил пиксели на экране. Я видел вносимые мной в мир изменения. И, наверное, именно так я заразился программированием: с помощью чего-то эфемерного (ведь программу нельзя пощупать) я достигал видимого результата.

Да и само объяснение принципов работы компьютеров, услышанное когда-то в школе, вполне согласовывалось с обычным ощущением изменяемости нашего мира. Мол, есть память, состоящая из ячеек. Есть процессор, есть текущая позиция в памяти. Процессор берет значение из ячейки, текущая позиция сдвигается... И т.д., и т.п. Как-то без проблем стало понятно, почему память конечна, и почему целое число может содержать значения только в каком-то диапазоне. Реальный мир, ничего с ним не поделаешь.

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

Благо, в университете вначале обучение велось на Turbo Pascal, в котором модули были на уровне языка (помнится, они были слизаны с Modula-2). Поэтому вхождение в структурное и модульное программирование произошло быстро и незаметно.

Затем я переключился на C. Хотя сам C мне не нравился. После Turbo Pascal он был какой-то сильно замороченный, да и компилировался на порядки дольше. А осенью 1991-го я раздобыл у знакомого книгу “Язык программирования C++” (первое издание) в электронном виде. По ходу ее первого чтения я даже не отдавал себе отчета о том, что это не C, а другой язык (вероятно, я просто не распечатал “Введение” из книги). На полном серьезе: я полагал, что книга описывает просто новую версию языка C. Поэтому был очень удивлен, когда Turbo C 2.0 оказывался компилировать мои программы с оператором new :)

Так вот, о переходе к ООП. После Паскаля в C мне очень не хватало такой простой и нужной вещи, как множества. В Паскале можно было объявить, например, множество символов и проверять, есть ли там какой-то символ. В C множеств не было, что вызывало у меня жуткий дискомфорт. Зато в Паскале нельзя было написать собственную функцию, получающую переменное число аргументов. Стандартные Write и WriteLn могут получать разное количество аргументов, а мои собственные функции - нет. Т.е. Паскаль не позволял программисту достигать того, что могли сделать разработчики компилятора Паскаль. А в книге я читаю, как в C++ средствами самого языка можно организовать множество. И что это множество будет вести себя точно так же, как и “зашитые” в язык вещи (вроде int). Вот тут-то я и попался на крючок C++. Этот язык давал пользователям языка те же возможности, что и своим создателям.

Само понятие класса в C++ для меня стало аналогом понятия unit из Turbo Pascal. В общем, такое же средство обеспечения модульности, только чуть в других масштабах и с несколько другими возможностями. Кстати, до сих пор очень жалко, что в C++ нет таких модулей, как в Turbo Pascal :(

Итак, сам того не подозревая, я стал последователем ООП, но исходя, первоначально, из соображений модульности. Если мне нужно было какое-то понятие в программе, которое имело собственные внутренние данные, то это понятие оформлялось в виде класса с закрытыми/защищенными атрибутами и публичными методами. Потом уже дело как-то дошло и до виртуальных методов, и до переопределения поведения в классах-наследниках, и до интерфейсов, и всего прочего. В конце-концов, до понимания того факта, что я использую именно ООП.

Таким образом, я на собственном опыте убедился, что ООП - это вполне естественная и интуитивно понятная концепция. И что при обучении императивному программированию (т.е. изменению состояния программы через прямые команды) и ООП мы на каждом шагу видим совпадения с тем, что происходит в реальном мире. Именно поэтому обучение императивному программированию идет настолько просто.

В отличие от функционального программирования, которое строится на абстракциях. А абстракции, это такая хитрая штука… Абстракции на то и абстракции, что их сложно объяснить на пальцах, на примерах того, что можно увидеть за окном. В моем случае, проблемы с математикой начались с возникновения в школьной программе пределов, дифференциалов и интегралов. Предел - это абстракция. Для меня предел - это пустой звук. Я не могу себе представить предел ни в виде какого-то предмета, ни в виде рисунка. Поскольку я не могу понять, что это, то я не могу представить себе ни зачем он нужен, ни как его использовать. Выучить вид пределов и операции над ними можно. Но понимания-то нет. А значит, нет и использования.

Так вот, возвращаясь к ФП. Программа, состоящая из функций. А функции не производят побочных эффектов. Мы запускаем функцию несколько раз и получаем один и тот же эффект. Поскольку вокруг ничего не менялось. Т.е. мы нарисовали линию на бумаге. Стерли ее. И нарисовали линию еще раз. Точно такую же. Абсолютно точно такую же. Но ведь это же противоречит тому, что мы познавали в различных проявлениях с самого детства - любое действие изменяет мир вокруг нас. Итак, первое противоречие с практическим опытом: функция - это абстракция, которую сложно объяснить на пальцах.

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

Более того, поскольку второе противоречие существует объективно, в программах на функциональных языках нужно как-то выделять фрагменты, которые отвечают за производство побочных эффектов. И тут на арену выходят монады. Еще одна абстракция, которую не объяснишь на пальцах…

Что ж, пора закругляться.

Императивное программирование настолько хорошо воспринимается людьми потому, что его принципы согласуется с тем, что человек наблюдает вокруг себя. ООП для меня интуитивно и понятно, поскольку я воспринимал его как продолжение структурности и модульности. А структурность и модульность – естественное следствие борьбы с размером и сложностью.

Функциональное программирование не интуитивно, поскольку его принципы противоречат наблюдаемой мной изменчивости мира. ФП не очень понятно, поскольку оно базируется на абстракциях.

Собственно, в разгорающемся сейчас интересе к ФП меня больше всего волнует вопрос о том, компенсирует ли ФП затраты, понесенные на его изучение и применение. Ответа у меня нет, но есть весьма скептическое отношение к ФП.

Но и яро отрицать ФП я пока не берусь. Поскольку вспоминается аналогия из легкой атлетики. За время существования прыжков в высоту, техника прыжка серьезно менялась два раза. И нынешний прыжок-прогибом, с помощью которого были поставлены современные рекорды, очень далек от интуитивности и очевидности. Может быть, ФП и есть тот самый прыжок-прогибом? Хотя, если продолжать данную аналогию, то более вероятно, что программирование - это вся легкая атлетика, а прыжки в высоту всего лишь одна из ниш в программировании. И ФП будет ставить свои рекорды именно в этой нише.

2 комментария:

Анонимный комментирует...

Черт, ну просто прекрасно написано! Такое ощущение, что читаешь собственные мысли, только заботливо и аккуратно разложенные по полочкам! Подписываюсь под каждым словом!

pgregory

eao197 комментирует...

Спасибо за лестный отзыв. Значит потраченное мной на написание этого текста время не было потрачено впустую :)