Pull to refresh

Comments 215

fun countLinesInFiles(fnames: List<String>): Int
    = fnames.map { File(it).readLines().size }
        .sum()

Эта реализация считывает все содержимое файлов в массив только для того, чтобы потом взять его размер, тогда как "неправильная реализация" читает посимвольно.
Вам стоит попробовать обе на папке с логами в пару гигабайт и подумать еще раз над тем, какая их них правильнее

UFO just landed and posted this here

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

Это чистое имхо, сильно серьёзно не воспринимайте.


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


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

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

А могут и не быть радикально разными. И причём намного чаще так и есть. В подавляющем большинстве случаев паршивый код от вполне качественного отличается не затратами на разработку, а опытностью разработчика. Или просто степенью его ответственности.

Так разрые притерии существуют
В одной разработке критерий заказчика — чтобы сделать быстро, дешево, выкатить на прод, а на поддержку взять индусов.
В другой разработке — сделать так, чтобы запрос обрабатывался на 30мс быстрее, чем у конкурента.


В обоих случаях заказчик мыслит понятными ему сущностями, за них же и готов платить. И он прав, ведь деньги то у него. Нам только остается выбирать тот проект, критерии оценки которого нам больше по вкусу.

UFO just landed and posted this here
Насколько я помню, закрытие платформы Intanium — тоже вина компиляторов, которые не научились внятно оптимизировать код.
Чёртовы компиляторы!
Тут дело не в компиляторе, а в реализации библиотеки.

Я люблю ФП, но то как это читается в Kotlin — по моему мнению, полная каша

А самое главное: подобные способы решения задачи даже не требуют понимания, что же там творится внутри. А если не надо знать и понимать, то и учить незачем. Не то нынче молодое поколение программистов, ох не то… а что будет дальше и подумать страшно.
Николай Ромашкин, Бета-тестеры, примерно 2005 год.

— Вот я и говорю, — подавленно говорил он слегка заплетающимся языком. — Применение всех этих стандартных библиотек скотинит и развращает программиста. Низводит его до состояния быдла. Животного. Собаки Павлова. Загорелась зеленая лампочка — вызывай эту функцию. Загорелась синяя — другую. А почему?! П-почему лампочки загораются?!


Последнюю фразу Ксенобайт произнес с тоскливым надрывом. Гордо выпрямившись, ударил себя кулаком в грудь, потом снова сник.


— Н-никто не помнит. Никто не помнит, откуда там эти лампочки и почему горят. Ты меня понимаешь? Понимаешь, что я чувствую, когда смотрю на эти лампочки?


Собственно ничего не меняется)


P.S. Считаю вариант с циклом более читаемым и надёжным. Вот только константы -1 и 10 стоило бы так и назвать. Тем более что сравнение некорректно, можно было и императивно использовать BufferedReader.readline() (если это конечно Java).

UFO just landed and posted this here

Для написания. В цепочках вызовов легко сделать что-то не так (прочитать весь файл целиком например), забыть обработчик исключений (привет реактивная Java, собраться соберётся, но упадёт потом если что), или просто перепутать функцию (использовать flatMap без побочных эффектов, забыть dispose сделать и так далее). Так же императивный стиль просто физически не даст сделать вложенность операций на 30 (в функциональном стиле занимало 10 полных строк).


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


Ну и в целом, первый код понятен любому джуну, если что-то пойдёт не так — есть отладчик. А в функциональном стиле отладчик особо не поможет, джун пойдёт читать документации об потоках и застрянет надолго.


Это моё имхо, я могу и так и так, но предпочитаю писать проще, особенно если поддерживать не мне.

UFO just landed and posted this here
с каких это пор отладчик перестал помогать с лямбдами и стримами?

На этом моменте пустил скупую слезу...

И еще один неприятный сюрприз ожидает пользователей "правильной" версии, когда выяснится, что эта реализация падает с арифметическим переполнением, если в файлах, суммарно, окажется < 2,147,483,647 строчек, а sum умеет возвращать только Int.
При этом в "неправильной" версии можно использовать BigInteger

А что мешает использовать Integer и соответствующую функцию (например GHC.List.sum)? Да, пока что программист ещё должен думать о пределах типов данных.

Глянуть бы реализацию на хаскеле с посимвольным чтением
UFO just landed and posted this here
UFO just landed and posted this here
syscall заполняет буфер, а по буферу уже проходимся с uintptr_t*. И в конце ещё проверяем оставшиеся некратные символы.
UFO just landed and posted this here
А мы всё ещё про хаскель говорим? :)

С аппаратной точки зрения лучше читать по словам, чем полагаться на кэш и эффективное обращение к невыровненным данным.
В том же Python давно уже есть ленивые последовательности и они прекрасно подходят, в т.ч. для таких задач (потому что для таких абстракций как файл и пр. это реализовано)
Например этот же код будет выглядеть вот так:
from typing import List, Generator, TextIO


def count_lines_in_files(fnames: List[str]) -> int:
    files: Generator[TextIO] = (open(fname) for fname in fnames)  # c'est ne pas un loop
    lines: Generator[str] = (line for file in files for line in file)
    return sum(1 for _ in lines)


def count_lines_in_files_alt(fnames: List[str]) -> int:
    return sum(
        map(
            lambda f: sum(1 for _ in f),
            map(open, fnames)))


В обоих вариантах ни о каком считывании полного файла в память не идёт и речи!

ленивые последовательности в c#


long CountLinesInFiles(string[] files) => files.Sum(f => File.ReadLines(f).LongCount());

Простите, но у readLines под капотом bufferedReader, который не грузит весь файл в память
А насчет ленивой последовательности — вот так будет лениво


File("xxx").bufferedReader().lineSequence()
А for line in file разве не лениво?
разве тут не формируется хотя бы ненужная строка каждый раз? Можно сравнить на Питоне с просто подсчётом сивмолов конца строки
Не факт, что строка формируется — можно реализовать и без этого, но я, честно говоря, не знаю как оно на самом деле реализовано.
Можно написать иначе, чтобы было лениво, опции-то есть:

fun countLinesInFiles(fileNames: List<String>): Int =
    fileNames.sumBy { File(it).useLines { it.count() } }


Но все равно речь была немного не о том.
Более того, эта реализация скрывает реальный подсчет в библиотечном вызове.
Типа «ну, раз мы код не написали, а замели всё под ковер, то стало лучше».
Кто мешает подсчет в «плохой» функции также вынести «за скобки», в отдельную функцию? Это типичный прием, некорректный, который обычно применяют для демонстрации преимуществ stream-стиля: в итеративном стиле явно прописывается вся реализация, а в stream-примере кишки скрываются за фасадом библиотеки.

Так чтобы кишки один раз скрыть за удобными фасадами и потом легко, не обматывая дополнительными кишками, собирать из них что угодно, и нужен функциональный стиль (в идеале – чистый).

Ничуть. Пихайте себе функционал «прошерстить стрим базового типа и посчитать црлфы» в обычную императивную функцию-процедуру и вызывайте. И вот тогда будет честным сравнение читаемости stream-стиля написания и императивного.
Можно просто на папке с миллионами файлов. Тоже будет забавно.
Вам стоит попробовать обе на папке с логами в пару гигабайт и подумать еще раз над тем, какая их них правильнее


А вы не скажите какой вариант правильный не зная контекста. Все от задачи зависит. Если я знаю что функция будет применяться редко и (или) на малом количестве файлов, я забью на скорость и сделаю функцию максимально читабельной и friendly к дебагингу (к слову функциональный вариант не очень friendly).

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

Вы правы, не скажу. Зато автор безапелляционно утверждает, что циклы — неправильно, приводит пример в разделе "Как не надо писать" с подписью про "упоротость".
По сути, он взял эффективный, но сложный алгоритм, обозвал его упоротым и переписал, подняв читабельность и пожертвовав эффективностью.


Чуть выше, RandomInternetPerson привел вариант с ленивым построчным чтением, что, ИМХО, в 80% типовых случаев будет оптимальным

Эта реализация считывает все содержимое файлов в массив только для того, чтобы потом взять его размер

Тут нет никаких требований читать файл целиком и держать строки в памяти.
Функциональный вид позволяет читать построчно и делать любые оптимизации, например SIMD / чтение через mmap, как лучше для конкретной ОС. Вопрос действительно в адекватном компиляторе / реализации библиотек.
Впрочем это уже написано в статье. Вы видимо пропустили часть про «fusion»?

«неправильная реализация» читает посимвольно.

Поэтому является довольно неторопливой.
Новые SSD на PCIe 4.0 обеспечат чтение 7ГБ/с. Вы продолжите дёргать read() по байтику? Ужас-ужас.
Новые SSD на PCIe 4.0 обеспечат чтение 7ГБ/с. Вы продолжите дёргать read() по байтику? Ужас-ужас.

У меня на SSD со скоростью 2.6Гб/с сообщения в Скайпе подлагивают, кнопка «Пуск» не мгновенно открывает меню, а браузер основательно залипает, когда открывает страничку Хабра с парой тысяч комментариев, и жрёт этак гигабайт памяти. При этом, я помню, почти двадцать лет назад Word легко справлялся с абсолютно такой же по сложности задачей (рендеринг «на лету» документа с полутора сотнями страниц, таблицами, рисунками) на Pentium-233 MMX с 64Мб ОЗУ и смешным винтом на 6Гб с недостижимой пиковой скоростью интерфейса 66 Мб/с.
У вас что, есть хоть капля сомнений, что пофигизм разработчиков легко справится с любым, даже самым производительным железом?
У меня на SSD со скоростью 2.6Гб/с сообщения в Скайпе подлагивают

А какая связь? Соседи, долбящиеся в стену по утрам, вряд ли являются причиной почему скисло молоко.
Маловероятно что Skype пишет гигабайты в секунду на диск?

В любом случае, вот поэтому и тормоза, потому что используют подходы «в лоб», как пример — посимвольное чтение файлов через getc() и подобное.
А те, кого волнует быстродействие, строят вот такие велосипеды.
github.com/lemire/simdjson

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

Это не просто так. Браузеры неплохо оптимизированы и пишут их умные люди.

двадцать лет назад Word легко справлялся с абсолютно такой же по сложности задачей

Ну так смотрите интернеты в нём =)
UFO just landed and posted this here
можно ссылку на такую статью с комментариями? хочется тож посмотреть как тупит
UFO just landed and posted this here
В любом случае, вот поэтому и тормоза, потому что используют подходы «в лоб», как пример — посимвольное чтение файлов через getc() и подобное.
А разве такая фигня не агрегируется на уровне ОС/ФС или драйвера? Вроде уменьшение числа системных вызовов и обращений к оборудованию вполне себе очевидная задача, а я себя самым умным не считаю.
Какая?
1) Вы вызываете getc() 32 раза который по байтику читает из буфера, который каждые N байт вызывает fread() для чтения блока через системного кэша.
2) Читаете 256 бит одной инструкцией AVX2 из файла, отображенного на память.
Есть разница?
Я про такую банальную вещь, чтоб на уровне ОС N чтений по байту превращалось в чтение области в N байт на диске.
Так и есть — оно буферизовано, но все равно вы N раз вызовете функцию «прочитать байт из файла», которая вернет байт из буфера. Так что накладные расходы все равно будут — на вызов этой функции, проверку буфера и т.д.
Маловероятно что Skype пишет гигабайты в секунду на диск?

Это всего лишь другая сторона одной и той же «медали» — написанного через задницу кода. В случае Скайпа может быть даже основная глубина задницы находится не в самом коде Скайпа, а в Электроне, на котором его построили. Это даже хуже, так как код одного приложения — это проблемы одного приложения. А кривая архитектура библиотеки, это проблемы сотен приложений. В случае Электрона, например, главная проблема в том, что он вообще появился на свет.
Ну так смотрите интернеты в нём =)

Гы-гы.
Это просто замечание к тому, что все эти ваши «неплохо оптимизированы» — это ерунда. Они плохо оптимизированы. Очень плохо. Начиная даже ещё с самих стандартов, которые они реализуют. Например, CSS ведь почти все воспринимают как что-то само собой разумеющееся, разрабатывали их далеко не самые последние люди в индустрии. В результате мы получили дичайший механизм костылей. Хотите расположить элемент по центру по горизонтали в блоке? Есть свойство text-align, но о-па, работает только в инлайновых блоках. В обычном надо использовать свойства margin-left и margin-right со значением auto. Это ещё ладно, а для вертикальной центровки, минуточку, там вообще изначально нет выравнивания для обычного блока. Надо было переключать его в режим таблицы или прибивать границы сверху и снизу гвоздями, задавая их явно. А потом, лет через десять вместо того, чтобы просто добавить эту, вполне себе логичную, фичу в существующий набор атрибутов, ввели новый режим выравнивания, флексбоксы. И так везде. В CSS вообще правило «свойство = значение => эффект, который описывает значение» применимо только к простейшим свойствам вроде размеров и цвета. Основной принцип работы со стилями выглядит как «свойство 1 = значение 1; свойство 2 = значение 2… свойство N = значение N => получили производный эффект, зачастую побочный»
Поэтому не надо верить в умных чуваков, которые всё продумывают лучшим образом. Нифига. Даже в самых серьезных проектах за деревьями архитектуры никто не видит леса.

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

Поэтому является довольно неторопливой.
Новые SSD на PCIe 4.0 обеспечат чтение 7ГБ/с. Вы продолжите дёргать read() по байтику? > Ужас-ужас.

а) это очень поверхностное суждение, потому что:


  • оба варианта используют буферизированное чтение через BufferedReader. Сходите да посмотрите на readLines сами: https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/jvm/src/kotlin/io/ReadWrite.kt#L40
    Инами словами, "дёргать read() по байтику" совсем не означает считывать по байту за раз физически.
  • я же довольно ясно выразил свою мысль: проблема не в самом чтении, а в том, как считанные данные потом обрабатываются. "правильная" реализация создает списки, бессмысленные и бесполезные, захламляющие память и создающие нагрузку на GC (которые, кстати, еще и динамически растут, что приводит к перевыделениям памяти при создании каждого списка)

б) вы сами-то пробовали проверять?
у меня на самсунге 850 PRO (PCI-E 3.0) c тестовым набором в 43 128 файлов объемом 2.2 ГБ:


  • без кэширования (первый запуск): "правильная" версия: 27 сек. "неправильная": 23 сек
  • следующие запуски: "правильная" версия: 18 сек. "неправильная": 22 сек

Как видите разница в скорости небольшая и зависит от кэширования.
Зато "правильная" версия скушала 500мб RAM, а неправильная 0 (по крайней мере "на глаз" не удалось заметить выделений памяти)
А при увеличении объема правильная упала с
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded


вот вам и "ужас-ужас"

Название статьи не соответствует содержанию.

Что-то не так у них с индустриальным программированием. При индустриальном программировании рабочий горячего Цеха направляет Ковш, который Отливает Чугун в Форму, после чего Форму Везут в Цех ковки, где Куют колёса. Потом колёса Везут в Цех шлифовки, где колёса Шлифуют.


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

Тогда уж:
Рабочий(abst.class) Горячего_Цеха(class) Направляет(func) Ковш(class), который Отливает(func) Чугун(class) в Форму(class),
после чего
Форму(class) Везут(func) в Цех_Ковки(class), где Куют(func) Колёса(class). Потом Колёса(class) Везут(func) в Цех_шлифовки(class), где колёса(class) Шлифуют(func).

А теперь определите, пожалуйста, в чьей ответственности операции «после того» и «Потом»
У кого определены функции Направляет и Отливает, и что делать если при вызове «Везут» случилась ошибка памяти, стека, сетевого вызова, и т.п.
Абстракции хороши, но они все равно текут.

А почему вы считаете, что class и function — это основные примитивы? Ведь было время, и классов не было (а были структуры с указателями на ассоциированные функции), а было время — и функций не было.


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


(Я даже и не знаю, троллю я или нет — потому что лямбда-калькулюс — это как раз попытка альтернативного описания того же, что описывает машина Тьюринга, в которой нет ни композиции, ни применения. Вполне можно представить себе альтернативную онтологическую модель, которая бы описывала работу устройства без деления на "код"/"данные" и/или концепции стрелочек из теории категорий).

>потому что лямбда-калькулюс — это как раз попытка альтернативного описания того же, что описывает машина Тьюринга
Насколько я знаю, работа Чёрча появилась несколько раньше, чем Тьюринга. Так что это еще вопрос, чье описание альтернативное. Не настаиваю и могу ошибаться.
Тьюринг — ученик Черча. Так же как и Клини, который описал все то же самое что и Тьюринг но чуть более элегантно (правда немного позже). А лямбда калькулус — частный случай term rewriting system, которые придумали вообще не для программирования.
amarao >в которой нет ни композиции, ни применения.
Прямо с вики:
В основу λ-исчисления положены две фундаментальные операции:

Аппликация (лат. applicatio — прикладывание, присоединение) означает применение или вызов функции по отношению к заданному значению.…

Абстракция или λ-абстракция (лат. abstractio — отвлечение, отделение) в свою очередь строит функции по заданным выражениям.…

Первое — это apply, второе — композиция. Просто другим способом (все через прямую подстановку) — но сути это не меняет.

На самом деле и модель Тьюринга, и работы Чёрча — теория, но Тьюринг реальную машину построил, так что он правее.


А потом пришёл фон Нейман, и все остальные модели под его вынуждены адаптироваться.


Напоминаю, что ни модель Тьюринга, ни лямбда-калькулюс не готовы к приходу прерываний. Что такое прерывание для лямбда-исчисления? Неконтролируемый рандомный сайд-эффект.

Но это не повод переходить на автоматное программирование для прикладных программ.
>Что такое прерывание для лямбда-исчисления? Неконтролируемый рандомный сайд-эффект.

Всего лишь символ на входе автомата. Все упирается в выбранную модель. Любое IO можно привести к строке символов на входе — надо лишь правильно задать алфавит.

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

Но в принципе мы же можем прерывание сделать детерминированным, чтобы оно обрабатывалось строго после инструкции? Задача прерывания же не в том, чтобы оборвать процессор на полу-слове, а в том, чтобы в принципе прервать выполнение. Да, для систем РВ это недопустимо, но теоретически-то?

Мы можем сделать так, чтобы прерывание выполнялось после текущей инструкции. Прерывание не будет детерминированным (мы не будем знать после какой инструкции оно произойдёт).


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


Каждая. Другими словами, чистых функций больше не будет от слова "совсем". Мы пишем


filterPrime [2..]
  where filterPrime (p:xs) =
          p : filterPrime [x | x <- xs, x `mod` p /= 0]

А эта функция — грязная, у неё есть сайд-эффекты. Да что там filterPrime, мы пишем a = 2 + 2, а эта функция — с сайд-эффектами.


Мне кажется, это не совсем модель в которой применим lambda-calculus.

Поясните, пожалуйста, откуда в a=2+2 сайд эффекты. Используются два регистра, которые используются эксклюзивно в данный момент времени.

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

Разве чистота про время (и даже последовательность) исполнения? Вроде достаточно не изменять данные снаружи функции. Или вы предполагаете, что мы из функции изменили стек и контекст процессора (EDIT: даже если потом вернули как было) и таким образом «нагрязнили»?

Если мы пытаемся описать машину с прерываниями с помощью модели без прерываний, то у нас каждая операция вызывает грязную функцию, которая может сделать return, а может и значение на стеке по доброте душевной поменять.

Можно сказать, что по планировщику у нас прерывается одна чистая функция и продолжается или начинается другая. Вопрос: можно ли написать «чистый» планировщик, потому что тут по-моему однозначно работа с глобальным состоянием.

А почему "другая" функция чистая, если она делает IO и меняет память чёрти где вне своего состояния?

Если функция берёт что угодно, не обрабатывая, по адресу, а потом точно так же кладёт обратно, почему она не чистая? Для любого входа она вернёт True, если копирование удалось, или False, если что-то пошло не так.

Я не совсем понимаю о чём вы.


Вот, простой код:


int foo(){
   int z=0;
   return z;
}
int main(){
    int x=foo();
    int y=foo();
    assert(x==y);
}

foo — это чистая функция, судя по написанному. А теперь представим себе, что у нас в момент после выполнения второго call foo случилось прерывание, обработчик которого запрограммировал DMA-адреса, перекрывающие .bss секцию, и устройство записало туда 42.


Дано: сработавший assert на ровном месте.

Это означает, что в системе есть UB. Наличие UB не нарушает чистоту функции.


Более того, так как из UB следует что угодно, наличие такого глючного обработчика прерываний делает любую функцию чистой (в случае гарантированного срабатывания прерывания, конечно же) :-)

Если верить документации процессора, то это не UB. Чтобы что-то объявить UB, нам надо иметь модель памяти (языка?) для которого это UB. С точки зрения документации к процессору (т.е. с точки зрения фон-неймановской архитектуры) у нас нет понятия "чистая функция", а есть конечный автомат процессора, в каждый момент времени которого у нас есть стрелочка на состояние "обработка прерываний", которая в свою очередь может делать что угодно.

Так ведь чистые функции именно что в языке существуют, а не в процессоре.

Понятие чистоты функции существует в модели, которая не может описать современные компьютеры.


Точнее, может, но моя версия почему-то у всех вызывает возмущение.

Точнее, может, но моя версия почему-то у всех вызывает возмущение.

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

Значит, мы о несколько разных вещах беседовали, для меня ФП — это методология написания программ. Что существующие компьютеры и компиляторы, которые позволяют безнаказанно залезть в чужую область памяти, не могут быть полностью чистыми, полностью согласен.

Тут важный момент — ФП — методология, которая придумывает себе понятие "функция" и "чистая функция", а потом компилятор должен в мучениях искать метод наиболее близко это сэмулировать на машине, которая ни сном ни духом про чистоту и даже функции поддерживает в Сишно-ассемблерном формате — CALL/RET, а что там на стеке и какие там сайд-эффекты никого не волнует.

Напротив, понятие чистой функции открывает больше вариантов для реализации: компилятор может делать с подобной функцией что угодно, лишь бы результат не изменился.

Тут есть два аспекта: что может (пытаться) делать компилятор и насколько его (и программиста) ожидания будут соответствовать поведению программы.


Где-то тут и находится страшный луркающий зверь UB, который и означает, что ожидания компилятора от ОС (и процессора) отличаются от реальности. Хороший компилятор объявляет UB всё, что будет UB и обещает, что если оно не UB, то будет как по модели ожидается. Совсем хороший компилятор про UB явно предупреждает или даже требует написать unsafe вокруг него.


Но прерывания — это момент, когда возникает UB, причём кто виноват в этом UB — это вопрос открытый, потому что про их существование в документации к процессору много написано.

Мы про условную норму, или про случай, когда кто-то в обработчике ошибся?
Применимость лямбда калкулус — это все что на нем можно выразить. На нем не надо писать реальный софт — это мат аппарат упакованный под капот компилятора.
Далее. Прерывания. Есть программа которая выполняется на процессоре. Есть прерывания. Описываете модель процессора (с памятью и регистрами, да). Пишете программу. Пишете компилятор, который компиляет программу в последовательность символов которая принимается моделью процессора. Символы — команды процессора. Это все стандартно и существует в природе. Далее. Эксепшены в модели процессора — всего лишь новый символ, на который процессор как то реагирует. То есть ничем от команды не отличается. Просто команда в коде — и мы сами решаем где она появится, а прерывание прилетает снаружи. Да. Но для модели процессора это пофигу — оно должно уметь его отработать. Проблема не в том что это не выражаемо через лямбда калькулус (в metamath квантовую физику завезли, а он работает на той же прямой подстановке что и лямбда калькулус) так вот, проблема в том что без автоматического вывода доказательств этот пируэт не имеет смысла — раз. Во вторых — для реального программирования придется строить модель cpu+память+ось в которой будет крутится софт, а вот с моделированием оси — засада. Более менее (с долей оптимизма) это возможно на формально верифицированных операционках, но таких полторы на всю планету и кроме одной — это pet проекты. Так что промышленно использовать это сейчас нереально, да. Но утверждать что IO либо внешние эвенты невыразимы через лямбда калькулус — неверно, прекрасно оно выражается, без монад и прочего. Надо просто строить модель того в чем будет крутиться программа вместе с самой программой. В Certified Programming with Dependent Types первый же пример очень близко подходит к этой теме (не раскрывает полностью но очень близко).
UFO just landed and posted this here

Вот я про "чистоту" и хочу сказать.


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

Если обработчик прерывания поменял значения в регистрах и так и оставил — это просто кривой обработчик, а не грязная функция.

Что такое "обработчик прерывания" в контексте описания чистой функции и почему он был запущен изнутри чистой функции? Либо эта функция не "чистая", либо у нас модель чистых вычислений не работает на оборудовании с прерываниями.


Это, кстати, нифига не теоретический вопрос, ибо куча приложений по модели должна работать хорошо, а на практике имеет непонятные задержки в местах, где не ожидает. (Условно — "чистая функция" на языке с GC может получить неожиданный сайд-эффект от GC из-за сложной сборки).

Время выполнения не является результатом функции (любой, не только чистой), а значит и замедление из-за GC сайд-эффектом не является.

Даже если время становится бесконечным? Кажется, это называется bottom type.

Не совсем. Тип ⊥ — это не просто бесконечное время работы или паника, а неизбежное бесконечное время работы или паника.

Давайте я скажу ещё хуже.


Вот у нас есть такая программа:


#include <signal.h>
#include <unistd.h>
int* c;

void handle(int signum){
    *c = 1;
}

void bottom(int *a){
    *a = 0;
    while(*a==0){
    };
}

void main(){
     *c = 0;
    alarm(2);
    signal(SIGALRM, handle);
    bottom(c);
}

(Я использую alarm для имитации прерывания).


Какой возвращаемый тип у bottom? (Я понимаю, что в Си это всё равно void, но по логике?).

А что такое a и как вы вызываете функцию не указывая её параметр?

Опечатка, пардон. bottom(c), конечно. Вопроса про a я не понял.


Адски кривой копипаст, поправил.

while(a==0) — что за переменная тут проверяется?

Ага, т.е. чистая функция возвращает IO. Я про это и говорю.

А с фига ли эта функция — чистая?

UFO just landed and posted this here
Хм. Когда это Тьюринг построил машину Тьюринга? Реальная машина, которую он построил, ничего общего с теоретической его же машиной не имеет. И вы, мне кажется, упоминали именно теоретическую его машину. Впрочем, это все равно были мелкие придирки к формулировкам, так что не важно

Не, я имел в виду, что он построил что-то работающее, так что его теории получают запас доверия больший, чем к теоретику, который не построил.


Грубо, но всё равно так.

Этот процесс отлично выражается в функциональном стиле:
СделатьКолёса Ковш =Направить Рабочий Ковш >>= Отлить Форма >>= Ковать ЦехКовки>>=Шлифовать ЦехШлифовки

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

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

Ну, например, посмотрите на машину тьюринга. Где там классы и объекты? Там даже данных нет, только переход по ленте.

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

А ещё ниже будет схемотехника, которая внезапно весьма ФП – у транзисторов нет состояния, а есть чистая зависимость выхода от входов, а состояние эмулируется, например, несколькими элементами, выходы которых поданы на вход друг другу – привет, рекурсия и tying the knot!
Только вот какая разница? Нам всё равно работать не под капотом, а наверху. А если уж порой приходится лазить под капот, стоит хотя бы пытаться минимизировать проведённое там время.

(вздох) У транзисторов есть состояние. С этим борятся, но оно есть. Ёмкость, распределение заряда в толще и т.д.

А ещё ниже работает квантовая электродинамика, в которой есть состояния (причем очень экзотические). Каждому уровню описания свои инструменты. Кто-то в верилоге делает логику, кто-то пишет операционные системы на Си, кто-то пишет бизнес-логику на языках высокого уровня в кровавом энтерпрайзе.

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

Мы, как программисты, думаем, что это мы пишем медленные программы. Но это не наша вина — это компилятор виноват.

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

Сильное заявление. Даже если у нас будет чудесный компилятор, который «строит хороший код, независимо от того, что написал программист», то встанет вопрос о времени, и прочих ресурсах, затрачиваемых на саму компиляцию. Чудес ведь не бывает. Чтобы убрать сложность из одного места, придется перенести её в другое место. А для всяких JIT и интерпретаторов, эта проблема будет ещё актуальнее.
Помимо уже упомянутых затрат на компиляцию, надо ещё учитывать, что любые абстракции протекают. Любой существующий компилятор, умеет оптимизировать код, лишь при соблюдении определённых условий, следующих из внутренней логики этого компилятора. А значит, что для получения желаемого результата, программисту придётся знать, и соблюдать эти условия.
Помню, в том же prolog'е, приходилось держать в уме весь алгоритм машины вывода. А ведь он как раз и позиционировался как: «Пиши как хочешь, а машина разберётся как это выполнять». Но реальность жестока. Приходилось писать так, как хочет машина, и разбираться, как она будет это выполнять.
В общем, если уж требовать от компиляторов, чтобы они были идеальны, и любой код, написанный программистом, превращали в высокоэффективную программу, то надо хотя бы доказать, что идеальный компилятор, в принципе, возможен. У меня в этом есть большие сомнения. Да и как-то это расходится с тезисом, озвученным в начале статьи:
Программирование — это тяжёлый труд, а «волшебных таблеток» не существует.

Разве идеальный компилятор, не будет той самой «волшебной таблеткой»?

Идеальный компилятор возможен, если временя компиляции будет стремиться к бесконечности.

Мне тоже интересно, если программист пишет квадратичный алгоритм там, где можно написать линейный, как поможет компилятор? Быстродействие кода в любом случае в первую очередь зависит от программиста, а лишь во вторую очередь от компилятора. Компилятор лишь инструмент. И этот инструмент нужно либо теоретически знать, либо практически исследовать его возможности. И не только компилятора, но и других низлежащих систем. Например операция создания объекта — дорогая. Операция же мутации поля в классе — дешевая. Я вот писал алгоритм с деревом, написал его и чистыми функциями и мутирующими. Тест производительности — вставляем миллион элементов в дерево. Максимальная глубина дерева при этом 20, средняя — 10. Т.е. для вставки мутирующему алгоритму надо создать 1 объект на итерацию, чистому — в среднем 10 на итерацию. Не считая балансировки. И на этой выборке, неожиданно, чистый код оказался чуть меньше чем в 10 раз медленнее. Как тут поможет компилятор реабилитировать ФП и чистые функции в частности? Непонятно.

Кстати, о квадратичных алгоритмах: SQL — очень хороший пример. Join двух таблиц равных размеров — это математически квадратичный алгоритм. Но в зависимости от ряда условий там и линейное время может быть. Сама идея, что нечто неоптимальное, но понятное в рантайме под капотом превращается в быстрое, непонятное, но при этом эквивалентное — это уже мейнстрим. Достаточно, к примеру на принцип работы V8 посмотреть

UFO just landed and posted this here

Не всегда. Hash join дает линейное время и на неиндексированных полях.

UFO just landed and posted this here

Нет, до этого СУБД сама догадается.


Тут скорее проблема в том, что в куче задач даже линейное время — это слишком много.

Гарантии О(1) и хеш таблицы в императивном коде не дают…
Просто потому, что компилятор слабоват.

Ну так почему бы не написать сильный компилятор на ФП, всем бы сразу нос утерли? К слову, компилятор Паскаля был написан на Паскале.
UFO just landed and posted this here
То есть, компилятор не слабоват на самом деле и автор заблуждается?
UFO just landed and posted this here
К слову, компилятор Паскаля был написан на Паскале.

Будто сейчас этим кого-то удивишь.

Да я и не пытался, я про то, что почему бы не написать хороший компилятор на Haskel, раз нынешний слабоват
вв статье не сказано, что это компилятор хаскеля слабый, там указано иное:
Любой программист мейнстрим языка сразу начнет кричать:
— Ужас, сколько тут циклов, сколько промежуточных структур данных… Это вообще не программирование! Вон из профессии!

Просто потому, что компилятор слабоват.

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

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

Твиттер Брагилевского(автор текста) — хороший инструмент для понимания того когда произошел троллинг.
Вообще-то быстродействие — это задача компилятора. В идеальной системе хороший компилятор должен построить хороший код, независимо от того, что написал программист. Если он строит плохой код, то это вина компилятора.

Это цитата из раздела «ФП — это очень медленно». Если это тоже троллинг, то так тонко, что порвалось.
Где там сказано что хаскелл компилятор генерит медленный код?
В контексте этого параграфа, там сначала претензия, «мы уже посчитали — а вы скомпилировать не можете» это не про скорость бинаря скомпилированного хаскеллем, это про скорость разработки. А дальше говорится про идеальный мир в котором компилятор должен строить хороший код независимо от того что написал программист. Тут указывается на проблему копилятора хаскеля что ему требуется хороший код, чтоб быть скомпилированным. В противовес тому, что хотелось бы чтоб любой говнокод(«мы уже написали, а вы еще пишете») компилировался в хорший код.
Чтоб читать Брагилевского нужно знать Брагилевского. Море двусмысленности и троллинга. Да, тонко что рвется, просто потому что вы не в контексте и это нормально, что вы не в контексте. ТК этот товарищ хоть и забавный и титульный, но все же немного маргинальный.
И я не защищаю хачкель, просто перевожу с брагилевского на русский.
Где там сказано что хаскелл компилятор генерит медленный код?

А где я утверждал, что компилятор хаскеля генерит медленный код? Сами придумали, сами ответили. Я только цитировал статью
>Где там сказано что хаскелл компилятор генерит медленный код?
>что почему бы не написать хороший компилятор на Haskel, раз нынешний слабоват
ок, слабоват.
Но в статье к которой вы комментите
— Ужас, сколько тут циклов, сколько промежуточных структур данных… Это вообще не программирование! Вон из профессии!

Просто потому, что компилятор слабоват.


контекст в голове не удержали, бывает

теперь и разговора не получится
Вот опять, сами процитировали, а теперь мне приписываете. Слишком много контекстов в голове, бывает.
UFO just landed and posted this here
Функциональное программирование — интересная штука. Не призываю всех переходить на него, но его элементы мигрируют во все обычные ЯП. И это прекрасно.

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

А это плохо, особенно если в команде больше одного программиста, или высокая текучка.

Поэтому — пишите в функциональном стиле на функциональных языках, ненадо тащить это в меинстрим.
>> фукнционал размазывается по разным местам и становится непонятно что и где происходит,

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

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

>> Все, можно сказать ваш дизайн больше не ваш

Выбор библиотеки — ваше решение. Значит дизайн ваш. То есть вы согласились с насилием со стороны библиотеки и оправдываете её применение каким-то другим преимуществом.
А потом вдруг решили использовать библиотеку, которая лезет в ваши объекты с помощью сопоставления с образцом...

А как это вообще возможно, хотя бы теоретически?

если найти их положение и размеры в памяти
Этому человеку уже один раз объяснили, что паттерн матчинг не имеет ничего общего с тем, чтобы лезть в потроха объектов. Но он не слышит, потому что не хочет. Ну не стоит оно того, чтобы убеждать.
Сборщик мусора изначально появился в функциональном языке, и только спустя двадцать с хвостиком лет начал мигрировать во все обычные ЯП. Не надо обобщать.
Сборщик мусора это другое дело, он же появился как замена, а не как дополнение. У программиста в Java нет выбора, использовать его или нет, он может его только настраивать.
А у программиста python, например, такой выбор есть. Ну а тернарный оператор, или цикл foreach, или еще миллион плюшек, которые вы используете каждый день и не задумываетесь о фп, тоже зло?

Ничего подобного. В ФП куда активнее используются абстракные интерфейсы, а паттерн-матчинг по заранее неизвестным типам невозможен за редкими исключениями (generics). Кроме того в ФП куда меньше распространён runtime reflection, который является верным путём к тому, чтобы сделать сложный, непонятный, магический, нечитаемый код.

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

Вам не стоит читать лекции по общей алгебре. Без понимания примитива типа «операция» и её свойств сразу кидать студентам пример гораздо более обвешанной ограничениями ситуации — это профанация. Математика не терпит поверхностности, а вы именно максимально поверхностно знакомите ваших студентов с предметом, что очень плохо. Кратко и без формализма вы лишь путаете студентов, создаёте ложное впечатление понимания. Нужно именно формально определить конкретную группу, а далее дать несколько примеров операций (включая ту же конкатенацию строк) и попросить доказать, каким ограничениям соответствуют предложенные операции, из чего вывести принадлежность к конкретной группе. Если же это кажется сложным, значит вашим студентам совершенно не нужна теория групп и абстрактная алгебра, ибо они её никогда не поймут по таким лекциям. Уж лучше не мучить людей лишними знаниями. Хотя конечно, если они платят за видимость образования, то…

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

>> Человеки обычно учатся индуктивно

Ну если вам нравится сначала изучить миллион примитивов, и только потом узнать, что все они сделаны по одному шаблону — учитесь так. Но на мой взгляд потеря времени будет совершенно бессмысленной, особенно если шаблон тривиален и укладывается в пару-тройку строк с определением.

И да, «индуктивно» учатся начинающие. После понимания дедукции взгляд на обучение сильно меняется, что я и рекомендую всем индуктивно мыслящим.

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

>> чем вываливать на студента ворох терминов в надежде, что он в них не захлебнется.

А с чего вы взяли, что я предлагаю вываливать ворох терминов? Я предлагаю не делать из людей идиотов попыткой убедить их в том, что они якобы с пользой отсидели в институтах 5 лет. И ворох терминов — один из вариантов получения идиотов на выходе. Только без понимания абстракций куча простеньких примеров точно так же делает из человека необразованного болванчика. Поэтому правильно учить людей «посередине», без вороха терминов, но и без потакания лени, когда ей не хочется учить абстракции.

А не надо изучать миллион, двух или даже одного примера достаточно. Но без них и правда плохо.

Если отбросить субъективное мнение об умении преподавать, то по большей части останется чистая правда. Математические основы не терпят поверхностности. Программирование больше склонно прощать — но результатом является обычно нечто не очень хорошо пахнущее.

"Формальное математическое определение моноида, но кратко и без формализма" — это вообще перл.

При этом достаточно формальное определение моноида будет примерно таким — множество, с заданной на нем бинарной ассоциативной операцией (и немного про свойства операции), и так вполне простое, зачем его еще упрощать? Какое из четырех существенных слов в этом определении не очевидно?

"Множество" — вообще неочевидно. Студент видел string'и, видел int'ы, видел class'ы. "Множества" студент не видел.
Что такое "ассоциативная операция" — тоже стоит пояснить.
Интуитивно можно понять только "бинарная операция", да и то не уверен, все ли студенты поймут это правильно.


В итоге вместо того, чтобы дать студенту чувство уверенности, такой формализм его только запутает.

Не знаю как вам, а мне понятие множества давали в школе. Как и понятие ассоциативности, и бинарности. Если студент видел инты и стринги, но не учился в школе — ну пусть поучится. Такому моноиды точно пока не нужны.

Моя цель как преподавателя — объяснить тему так, чтобы студент ее понял.
Стараюсь не решать за своих студентов, что им нужно, а что нет.

А откуда вы знаете, что им нужно? И почему вы уверены, что они поняли? Я тут в общем не критикую ваш подход — в конце концов, это вы общаетесь со своими учениками, знаете их лучше. Что им моноиды не нужны — не более чем мое мнение, вполне возможно неправильное.

Так что тут вопрос совершенно без подвоха.

P.S. Когда я преподаватель — мой опыт обычно больше, чем у учеников. И в каком-то смысле я лучше знаю, что им нужно. Не вообще в жизни — а в рамках именно этого курса.
А откуда вы знаете, что им нужно?

Во первых по содержанию курса, раз уж они на него пришли :)
Во-вторых — по задаваемым вопросам.


И почему вы уверены, что они поняли?
Уверен? Нет. Но в целом, у преподавателя есть два средства. Во-первых, это реакция учеников на объяснения. Во-вторых, результат практических заданий.

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

>А формальные объяснения из category theory этому мало помогут.
На мой взгляд, за формальными объяснениями должны следовать либо доказанные полезные свойства (то есть, какие гарантии мы скажем имеем, если формально докажем, что тут имеет «моноид»), либо показывали бы, как эти свойства самостоятельно вывести. Ну или куда дальше копать, если понадобится.
Такому моноиды точно пока не нужны

Вы уверены? Одну и ту же вещь можно объяснить как простым, так и сложным языком, как понятным, так и непонятным человеку. Я вот вышестоящее определение не понял (хотя и знаю эти слова по отдельности), но почти наверняка использую и моноиды и многие другие примитивы из ФП последние лет так 5. Выполняю задачи бизнеса.


А ещё через лет 5 практики я забуду те последние крохи формального языка, вбитого мне в школе и университете, что я ещё помню.

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

Ну то есть, если у студента проблемы с восприятием довольно несложного формализма, которые в общем-то нужен не всем и не каждый день — ну зачем им его вообще давать?
ну зачем им его вообще давать?

Да, это пожалуй тема для отдельного холивара. Подача материала. Как мне показалось именно матан страдает от этого в наибольшей степени. 90% времени при его изучении ты пытаешься понять что вообще автор учебника или твой препод имел ввиду, а по итогу задаёшься вопросом — неужели нельзя было эти, казалось бы не такие уж и сложные вещи, объяснить нормальным человеческим языком (а уже потом можно сформулировать "формальное" определение, а не наоборот). Зачем такой педагог вообще нужен. Правда это возможно только мой такой печальный опыт.


Мне нравится американский подход к подачи информации. Когда на простых примерах, простым языком объясняется как, что и почему работает. Не стесняются написать 10 абзацев вместо одного, убрав все головоломки. Примеров с edge case-ми побольше привести. Аналогий.

Практические примеры это хорошо, но пожалуй при одном условии — что упрощение не делает пример неправильным. Как с тем же моноидом — что значит «компоновать строки»? Например, применение регулярного выражения (в виде строки) к строке — это что за операция? Получится ли у нас моноид, если мы в качестве операции возьмем эту, а не конкатенацию?

Ведь по большому счету, в программировании весь этот формализм нужен для того, чтобы убедиться (доказать), что наша композиция функций, к примеру, всегда будет вести себя правильно. Можно ли жить без этого? Естественно, ведь живут же, во многих случаях и на многих языках, заменяя доказательство скажем тестами. Но чем выше уровень абстракции конструкции — тем больше хочется доказательств.
Правда это возможно только мой такой печальный опыт.

Не только :)


Зачем такой педагог вообще нужен.

В моем случае (Тель Авив), такие педагоги были в основном профессорами, которых заставляли давать лекции, "отвлекая" их от "важных исследований".

UFO just landed and posted this here

Собственно о чем я и писал — формальное определние давать не стоит, оно нужно не всем.

Изначальная претензия к авторскому примеру неформального определения была не потому, что оно неформальное — а потому что оно, в общем-то, не совсем правильное. Произвольная композиция строк вполне может быть не ассоциативной, и не иметь «единицы». И не будет там никакого моноида.

А мое дополнение состоит ровно в том, что и нормальное-то формальное определение моноида — это математика, вполне доступная на уровне средней школы (7-8 класс), то есть несложная.

Можно-ли/нужно ли без формализма — ну опять же, я уже отвечал, что не знаю, зависит от целей, да. Но с другой стороны, хорошего и краткого неформального определения, из которого можно было бы делать полезные выводы о свойствах, я не припомню. Что возможно означает, что его не так просто придумать.
Попадем ли мы на «head», вызванный для пустого списка?

Да запросто.


example minBound

Отрицательный модуль числа — вполне реальная штука, когда мы имеем дело с дополнительным кодом (two’s complement). И мне кажется, что это баг в статическом анализаторе, раз он считает, что 0 <= n.

Ну, скажем, в Rust такие вещи отлавливаются при компиляции (правда, только в константных функциях) и, если всё же просочились в код, приводят к панике в рантайме. В Си, если я правильно помню, отрицание с переполнением — это UB. Не берусь говорить про Haskell, но допускаю, что там такой код тоже будет семантически невалиден.

Тут фокус именно в том, что, насколько я знаю, в Haskell нет UB или исключения при переполнении, а значит код совершенно валиден...

Если копнуть глубже, то видно, что особой разницы между ООП и ФП нет. ООП — практически то же, что и ФП.

В ФП данные и функции существуют отдельно.

В примере из статьи, ООП написано как ФП, поэтому не нашлось разницы.
На самом деле в описанном классе не хватает тех самых данных, которые будут отличать ООП от ФП, а конкретно — можно закешировать внутри объекта результаты вычислений радиуса. Именно это является ключевым отличием, а не синтаксис. В ФП нет таких отдельных маленьких сущностей с состоянием, его приходится хранить отдельно в глобальных структурах и прокидывать для вычислений внутрь каждой функции.
Конечно, если у нас в программе нет закешированных данных и есть лишь небольшой объем входных, т.е. каждое значение мы считаем заново, то использовать парадигмы, предназначенные для кеширования результатов вычисления глупо.
Например, во фронтенде сейчас это прекрасно видно на примере React+Redux, когда Store становится большим, он становится поистине God-object и весь выигрыш в чистых функциях и едином месте, где видно состояние всего приложения пропадает.
А когда в дело вступает многопоточность — идея глобальных хранилищ начинает еще сильнее трещать по швам, в случае редакса опять же прекрасно видно на примере асинхронных экшнов.
Идея ООП хранить данные не в одном месте, а в виде графа, распределенного по всей программе не лучше и не хуже идеи хранить данные в одном месте.
В ФП нет таких отдельных маленьких сущностей с состоянием
А чем вам каррирование не подходит?
Наверное тем, что каррированию, как хранилищу, нельзя дать имя, реализовать синглтон с произвольной точкой доступа таким образом тоже невозможно. В итоге все равно все сводится либо к прокидыванию аргументов через всю цепочку вызовов, либо к прокидыванию ссылки на именованное хранилище.
ООП не обязано, в отличии от ФП, быть деревом, оно может быть любым видом графа, а в случае метапрограммирования, еще и многомерным. (В функциональный Реакт это заставляет вводить Context, в дополнение к глобальному стору).
эээ… а почему нельзя и невозможно?
Вот интересно, каррированная функция — это такая же функция, как любая другая, она может быть безымянной — а может и не быть. Присоединюсь к вопросу — что вы имели в виду, когда сказали, что каррированию нельзя дать имя? Ну то есть, я может буду готов с этим согласиться, если вы говорите допустим только о React+Redux, но обобщать я бы не стал бы.
В случае класса со свойствами (или глобальными переменными, или переменными модуля), мы даем имя этим переменным и складываем туда результат вычислений. В случае ФП — это всегда константы, т.е. мы можем «сохранить» результат вычислений с помощью каррирования и дать на него ссылку, но не можем подменить находящееся там значение. В этом есть свои плюсы и минусы. Для программ с большим числом вычислений и малым хранилищем (по времени), это позволяет писать чистые функции, не беспокоясь, что кто-то где-то как-то подменит значение, не нужно мокать для тестов глобальные объекты, объекты по ссылкам и т.д.
Однако, для программ с необходимостью хранения множества объектов с часто изменяющимся внутренним состоянием — удобнее, когда имя дается один раз, а значения под этим именем меняются.
Это как если бы мы дали одно и то же название любому каррированию этой конкретной функции, вне зависимости от аргументов. Чего мы сделать в ФП не можем, да и зачем.
Ну, да, в таком уточненном смысле — согласен.
Иногда да, иногда нет. Вообще-то быстродействие — это задача компилятора. В идеальной системе хороший компилятор должен построить хороший код, независимо от того, что написал программист. Если он строит плохой код, то это вина компилятора.

Хороший компилятор, внезапно, тоже программа. Если компилятор будут создавать программисты с таким мировоззрением, то он никогда не станет хорошим.

Я даже больше скажу, если программист допустит ошибку, хороший компилятор должен догадаться, чего имел ввиду программист и исправить ее.
он имел ввиду что очень хороший компилятор построит свой хороший код и догадается где в своём коде ошибки))
>В Haskell я напишу две функции, а не одну, потому что это функциональное программирование.
Я и в Java напишу две функции. Разделение на чистые функции и IO — оно вовсе не потому, что хаскель, а потому что это практично.
В идеальной системе хороший компилятор должен построить хороший код, независимо от того, что написал программист.
Только один вопрос: а программист тогда зачем нужен?
Кто-то должен писать хороший компилятор в идеальной системе.

Да этому стебу уже лет 25 на моей памяти. Уже что-то нового придумать очень сложно. И главное, смысла — ноль. Все как было, так и останется. Потому, что всему есть своя область применения. Отсюда разнообразие языков, стилей, методик, парадигм, методов и т.п.

Очень тяжело читать, потому что видишь вот такое заявление:
Если копнуть глубже, то видно, что особой разницы между ООП и ФП нет. ООП — практически то же, что и ФП.

и думаешь, вот сейчас то начнется, но потом появляется:
ООП — это работа с этой таблицей по столбцам, где каждый столбец — один класс. ФП — это работа с таблицей по строкам, потому что речь идет о функциях.

Какая разница, как работать с таблицей и как на нее смотреть: через строки или столбцы? Для соответствующих концепций это не имеет значение. Это важно только для нас как наблюдателей.

Оказывается, оба подхода — одно и то же потому что решают одну и ту же задачу? Или потому что на функциональном ЯП был реализован ООП подход? Мне кажется, что ФП заключается не в том, что функция определяет тип получаемого объекта и выбирает для этого нужный код. Я бы написал вот так:

Square=функция по определению хар-ых точек из длины стороны
Circle=функция по определению хар-ых точек из радиуса и шага по углу
Area=функция по определению площади по хар-ым точкам
Diameter=функция по определению диаметра по хар-ым точкам


И в результате получается что площадь считается как: Area(Square(5)). И заметьте, никаких равно, никуда мы это значение не сохраняем — площадь — это именно функция, а не цифра.

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

Что-то я не могу сообразить, как try/catch может помочь при отладке
Что-то я не могу сообразить, как try/catch может помочь при отладке

Я мимо проходил, но в случае ошибки в catch можно прозрачно для внешнего кода вывести все нужные нам данные подробно, записать в лог или отправить почту.

Так есть же всякие отладочные trace и даже специальный Logger в Хаскеле для таких вещей.

Не бряку поставить, ни код вписать для логирования, ни трай кетч.

А вдруг это все правда станет не нужно в настоящем ФЯ без mutable и прочего? Нет никакого зависимого окружения. Берешь любой кусок кода, отправляешь в REPL и получаешь результат работы. Не надо пол часа компилить, разворачивать базу нужной версии, поднимать инстанс, настраивать окружение, расставлять бряки чтобы отловить какую нибудь глупую ошибку в имени переменной.
После таких статей все больше убеждаюсь в том, что чистое ФП — наследие прошлого.
Это ты можешь откаррировать, а это нет. Эти два аргумента разделить нельзя, они должны всегда идти вместе. Тут скомпилируем, а здесь — нет


Пример можно?
Сейчас код выглядит подобным образом:


fun countLinesInFiles(fnames: List String): Int
= fnames.map { File(it).readLines().size }
.sum()


причина по которой код так выглядит — потому что есть готовая функция readlines ещё и сама считает поэтому есть size, а если писать построчное чтение с нуля будет похожее мясо правда с 1 циклом.
Правда лучше написать сразу split или count chars

Разница между ООП и ФП
Чтоб закончить срач поповоду ФП, задолбали уже.
Самый весомый признак ФП — это возможность присвоения переменной значения «функции» и использование переменной-функции в функциях

высокого порядка (и ещё ф-я может возвращать ф-ю)
f = какая то функция
g (f,x,y...)
И ВСЁ
замысловатые нотации типа чточто-> ещё что то -> вырвиглаз могут присутствовать в ФП но совсем не обязательны, это скорее фича декларативного програмирования. Как и head — tail понятное дело не будет толком работать императивно. Тут путают понятия и доказывают что декл. язык низкого уровня не пригоден для индустиаальных проэктов, открыли америку.

впринципе момент старого доброго С где поинтер на функцию был void* и передавался и возвращался как переменная. Насколько я знаю, это используется сплошь и рядом в любой индустриальной либе на С

UFO just landed and posted this here
В любом тьюринг-полном языке можно писать в любой парадигме, вопрос только в количестве затраченных усилий. Например, с помощью сишного препроцессора можно программировать функционально, но обычно так делать не надо.
Совершенно верно, возможность создавать функции высшего порядка не делает язык пригодным для ФП. Помимо возможности должно быть еще удобство и читабельность.
Самый весомый признак ФП — это возможность присвоения переменной значения «функции» и использование переменной-функции в функциях

Неверно. В ФП нет самого понятия присвоения. Всё иммутабельно.

Есть такой вопрос. Хочу преобразовать функцию для одного аргумента в функцию для нескольких:
function doSmth(a: number): string { // do smth … }
function doSmthForAll(a: number[]): string[] { return a.map(doSmth); }
то бишь вместо того, чтобы писать вторую функцию руками, хочу написать вот так, но название forArray мне не нравится
const doSmthForAll = forArray(doSmth);

у этого есть название в ФП?

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

Ну, вообще-то эта операция называется map :-)

так же как и метод на массиве?

Так в ФП нет никаких методов, вот в чём проблема. Каррированная же функция map без проблем частично применяется к другой функции, работая точно так же как ваш forArray

вот такое еще преобразование
function doSmth(a: number, b: number): string { // do smth … }
function doSmthForAll(a: number[], b: number): string[] { return a.map(a1 => doSmth(a1, b)); }

const doSmthForAll = partialMap(doSmth);

partialMap лучше подходит?
а что мешает сделать обертку над такими функциями типа
function carryTail (f, ...tail){
  return (arg) => {
    return f.apply(null, [arg].concat(tail));
  };
 }


а потом использовать обычный array.map от carryTail(doSmth, b)?
UFO just landed and posted this here
Рискуя быть закиданным несвежими логами, хотел бы обратить внимание на Scala. Года два назад начал на ней писать (спасибо курсу М.Одерски) и получил второе дыхание. Перетаскиваю команду потихоньку — самые сложные части системы уже на Scala (команда и раньше ее использовала со Spark, я помог индустриализировать и унифицировать старый код).
всякую хрень авторы сочиняют про функциональное программирование. Вообще что такое функция и для чего ее придумали? для того чтобы один и тот же кусок кода применялся несколько раз. и нафига тогда создавать такую функцию:
$f = function()
{
echo 'lol';
};
пишите просто echo 'lol'; в коде… Я просто в шоке. Заглянул в популярную библиотеку Query и увидел
function (e, n) { t[n] = !0 }
одни маты только…
пишите просто echo 'lol'; в коде… Я просто в шоке.

Не всегда это возможно. Например, если есть другой кусок кода, который ожидает именно функцию — как вы ему ваш echo 'lol' передадите не создавая функцию?

fun countLinesInFiles(fnames: List<String>): Int
    = fnames.map { File(it).readLines().size }
        .sum()

Это типа «нормальный» подход? Для студня может быть нормальный, а на практике нужно будет явно отреагировать на неудачу открытия файла и предпринять какие-то действия: простой выход, диагностическое сообщение, откат состояния и пр.
У меня, как математика по образованию, функциональный подход натуральным образом никогда не вызывал никаких затруднений, мозг заточен именно под такие конструкты. Но многолетняя практика в разработке ПО шепчет в ухо, что не стоит использовать функции с сайд-эффектами в map-ах и в итоге эти мапы имеет смысл применять там, где ещё лучше воспользоваться list comprehensions.
Sign up to leave a comment.