LINUX.ORG.RU

Функциональные линзы

 


0

2

Расскажите, пожалуйста, про них.

Мне не ясно, как они удерживают в поле зрения объект целиком.

Я пытался попытать ИИ:

Термин «функциональные линзы» (или просто линзы) пришел из математики (теория категорий) и стал популярен в функциональном программировании.

Линзы решают фундаментальную проблему функционального программирования — доступ к данным без мутаций. Они позволяют писать код, который:  
- Декларативен: программист говорит ЧТО изменить (почтовый индекс пользователя), а не КАК (скопируй юзера, потом скопируй адрес, потом...).  
- Композируем: Маленькие линзы (город, улица) собираются в большие (адрес целиком).  
- Типобезопасен: Если удалить поле из модели, линза сломается на этапе компиляции, а не в рантайме.  

Линза — это композиция двух функций для конкретного поля:  
Getter (view): Функция, которая достает значение поля из структуры.  
Setter (set/over): Функция, которая берет структуру и новое значение, и возвращает новую структуру с измененным полем.  
Объединив их в пару, мы получаем линзу: объект, который «смотрит» в конкретную часть данных (фокус).

Основные операции:  
view: Посмотреть, что лежит в фокусе (просто получить значение)  
set: Положить новое значение (Вернет новый объект с измененным полем  
over: Применить функцию к значению в фокусе.

Кроме линз есть целое семейство оптических инструментов (optics).  
Линза — это просто «глаз», который смотрит в поле структуры.  
Призмы (Prism) умеет «пробовать» достать данные, а если их нет — ничего не делать или переключиться на другой путь. Работают с типами-суммами (например, Maybe, Either или своими вариантами).  
Траверсалы (Traversal) позволяют фокусироваться сразу на нескольких элементах внутри структуры (например, на всех элементах списка, на всех значениях в Map, на всех полях определенного типа в записи).  
Индексированные оптики (Indexed Optics) позволяют при фокусировке на элементе коллекции также получать его индекс/ключ.  
Изо (Iso) представляют собой взаимно-однозначное преобразование между типами, которое можно обратить.  

композиция оптик — их все можно соединять друг с другом. 

Комбинаторная логика - это система, эквивалентная лямбда-исчислению (по выразительной силе), но в ней нет переменных и связывания (лямбда-абстракции). Она использует базовые комбинаторы (например, S, K, I - подстановки, константы, идентичность).

Но ничего не понял, из того, что он там понаписал.

Что я хочу сделать: я хочу загрузить файл в память, распарсить его, а затем некоторые части заменить (например некоторые слова выделить жирным в HTML-тексте), после чего весь модифицированный файл сохранить.

Если примеры, то мне на Java будет понятнее всего.



Последнее исправление: maxcom (всего исправлений: 2)

на Java

Пфф, на жава линзы не нужны, это костыль специальная возможность, чтобы хоть как-то мочь «изменять» иммутабельные данные в ЯП, где нет мутабельных данных (Хаскель и др).

goingUp ★★★★★
()

Сейчас, говорят, модно в многоядерные процессоры. Там нужно.

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

goingUp ★★★★★
()

А к чему загружать целиком в память? Выглядит как задача для поточной(stream) обработки

cobold ★★★★★
()

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

goingUp ★★★★★
()
Последнее исправление: goingUp (всего исправлений: 2)

Что я хочу сделать: я хочу загрузить файл в память, распарсить его, а затем некоторые части заменить (например некоторые слова выделить жирным в HTML-тексте), после чего весь модифицированный файл сохранить.

А если не теряться в каких-то эзотерических (анти)паттернах программирования, и просто взять и сделать «в лоб»? Красивое всегда сможешь потом перепилить.

Bfgeshka ★★★★★
()

Если примеры, то мне на Java будет понятнее всего.

А нужен именно код? Просто я уже написал, одна работает через Wine, вторая нативная в Linux, но и без моих поделок в линукс можной найти такой софт (GUI и консольная sed).

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

Погуглил, в Java есть replace, replaceAll с поддержкой regexp. Чтение из файла тоже готовые функции File, FileReader/FileWriter, BufferedReader.

AZJIO
()
Последнее исправление: AZJIO (всего исправлений: 1)

Так попроси у ИИ примеры блин, а не описание.

Спроси лучше у него сразу Vavr для явы.

LightDiver ★★★★★
()
Последнее исправление: LightDiver (всего исправлений: 1)

Это что-то типа get-in/update-in из кложи, только на волшебных грибах типобезопасности?

Nervous ★★★★★
()

мне на Java будет понятнее всего

Лично я в каком-то таком виде понял

class Test {
    @Builder(toBuilder = true)
    @ToString
    public static class Data {
        private final String name;
        private final OffsetDateTime date;
        @Singular
        private final List<Integer> numbers;
    }

    public static Data SomeFn(Data data) {

        return data.toBuilder().name("New Name").number(42).build();

    }

    public static void main(String[] args) {

        Data b = Data.builder().name("Name").date(OffsetDateTime.now()).numbers(List.of(1,2,3)).build();

        System.out.println(b.toString());

        b = SomeFn(b);

        System.out.println(b.toString());
    }
}
TestPack.Data(name=Name, date=2026-03-13T10:06:32.478554285+03:00, numbers=[1, 2, 3])
TestPack.Data(name=New Name, date=2026-03-13T10:06:32.478554285+03:00, numbers=[1, 2, 3, 42])
vtVitus ★★★★★
()

Линза — это композиция двух функций для конкретного поля:
Getter (view): Функция, которая достает значение поля из структуры.
Setter (set/over): Функция, которая берет структуру и новое значение, и возвращает новую структуру с измененным полем.
Объединив их в пару, мы получаем линзу: объект, который «смотрит» в конкретную часть данных (фокус).

Вот в твоем случае «getter» это что-то вроде XPath выражения, позволяющее выдернуть конкретный элемент DOM дерева.

«Setter» в Java обычно не имеет смысла, потому что структуры мутабельные (например то же DOM дерево), и изменение можно непосредственно в них произвести. В ФП обычно струкруры не мутабельные, по этому вместо изменения нужно сделать копирование всего DOM дерева с заменой нужной тебе части. Setter это и делает.

Конкретно по твоей задаче см. https://jsoup.org/cookbook/modifying-data/set-html

maxcom ★★★★★
()
Ответ на: комментарий от goingUp

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

У вас в голове императивная модель гвоздями прибита. Мол, есть нормальный способ «цикл со счётчиком», а есть альтернативный обходной путь, когда нельзя по-нормальному «рекурсия». Тогда как на самом деле рекурсия и итерация — два равнозначных, равноправных способа вычисления. Вот, Хаскель уже затем изучать надо, что он ум в порядок приводит!

ugoday ★★★★★
()
Ответ на: комментарий от ugoday

рекурсия и итерация — два равнозначных, равноправных способа вычисления

Да, пока стек не закончится.

ya-betmen ★★★★★
()
Ответ на: комментарий от ya-betmen

«И тебя вылечат». Вы не то что бы совсем не правы, просто смотрите на проблему с очень узкой точки зрения. А это само по себе является ошибкой.

ugoday ★★★★★
()
Ответ на: комментарий от ugoday

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

cobold ★★★★★
()
Ответ на: комментарий от cobold

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

от когда у вас в телефоне будет не фоннеймановская машина

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

ugoday ★★★★★
()
Ответ на: комментарий от cobold

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

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

Waterlaz ★★★★★
()

Все эти ваши линзы напоминают удаление гланд через задний проход.

Psilocybe ★★★★★
()
Ответ на: комментарий от ugoday

Что уже опциональная возможность бодаться с суровой реальностью или принудительная необходимость быть от неё изолированным?

ya-betmen ★★★★★
()
Ответ на: комментарий от ya-betmen

Здесь. Умный человек задался бы вопросом: важна ли эта проблема, всегда ли важна эта проблема, есть ли другие важные проблемы в данном случае и т.п, а не вырвал бы один (безусловно верный) факт и носился с ним.

ugoday ★★★★★
()
Ответ на: комментарий от goingUp

Мутабельность вообще говоря проблема а не решение. Ты прямо либо не совсем понимаешь о чём говоришь, либо лукавишь. В целом если нет мутабельности, то нет многих ошибок, например

1) Нет Aliasing Bugs - когда ты передал список в функцию, ты на 100% уверен, что после вызова список остался прежним. Это делает код предсказуемым. В мутабельных языках протухшая где то в другом месте переменная - притча во языцах

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

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

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

Некоторое время назад группа исследователей из Пенсильванского университета сделала формальную верификацию основных библиотек из хаскелевских containers. В частности были проверены Data.Set (множества), Data.Map (словари), IntSet. В коде Data.Set и Data.Map не было найдено ни одного серьезного бага.

Это подтвердило что хорошо протестированный код на чисто функциональном языке по качеству практически не уступает формально верифицированному. Типизация и чистота функций сами по себе отсекают 99% проблем.

Есть также работы компании Galois, Inc., которая десятилетиями использует Haskell для оборонных и аэрокосмических заказов. Они подтверждают, что использование таких языков позволяет находить ошибки на этапе компиляции, которые в C++ или Java «выстрелили» бы только в продакшине.

Пенальти по производительности. Окамль к примеру где то на 30% медленне раст в реальных сценариях для веб, примерно на уровне с го.

Внутренние тесты сообщества Retro-httpaf-bench показывают, что современные серверы на базе библиотеки Eio (эффекты для OCaml 5) могут обрабатывать сотни тысяч запросов в секунду, приближаясь к результатам Rust/Hyper на многоядерных машинах.

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

По поводу монад - на самом деле как сказал один умный человек всё что мы хотим по большому счёту - это честность. Если мы делаем ввод вывод и работаем с переменным окружением - мы испльзуем IO в хаскеле. И программист и компилятор понимаю что здесь код не будет повторяемым от запуска к запуску. И т.п.

Если говорить о том что можно делать а что делают бедолаги - посмотри на эффекты 1-класса Ocaml5:

Положим, у нас функция для чтения группы файлов (пишу на питоне/псевдопитоне)

def read_files_content(file_paths):
    contents = []
    for path in file_paths:
            with open(path, 'r', encoding='utf-8') as f:
                contents.append(f.read())
    return contents


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

Понятно, что для задачи 2 всё должно работать, мы кидаем исключение и просто ничего не делаем. Что будет решением задачи 1? ну, условно, другая функция (ну или пляски).

Что позволяют эффекты в Ocaml: мы можем выполнить эффект (это как бросить исключение, только ключевое слово не raise а perform).
Условно, бросаем исключение, но в отличие от исключений, мы когда его ловим, можем сказать либо «бросить настоящее исключение», либо «вернуться в ту же точку программы из которой мы произвели эффект и вернуть пустую строку вместо контента файла»

Т.е. вернуться прямо внутрь функции на ,положим, пятую итерацию цикла for, понимаешь? Причём всё это работает со стеком, а не с кучей (как исключения обычно), соотвественно, работает быстро.

Выглядит всё это близко к конструкции try-catch в обычных языках, т.е. просто описываем что возвращаем:
//здесь говорим что надо вернуться и продолжить
try 
  content = read_files_content (file_list) 
catch 
  Exception -> return_eff ("", continue)

//а здесь просто игнорируем исключение, поймает кто то выше или вывалимся с ошибкой, всё как обычно
  content = read_files_content (file_list) 


Эффекты активно используются в Eio - окамлевской библиотеке ввода-вывода предназначенная для создания высокопроизводительных, конкурентных приложений.

Теперь вспомни свой го или на чём ты там пишешь. И про количество ошибок вспомни и про возможности.

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

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

ТС, линзами не пользуюсь, но люди любят.

AndreyKl ★★★★★
()
Последнее исправление: AndreyKl (всего исправлений: 2)
Ответ на: комментарий от AndreyKl

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

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

ugoday ★★★★★
()

Мне не ясно, как они удерживают в поле зрения объект целиком.

Зачем цепляться за волшебные и аурные понятия?

Что я хочу сделать: я хочу загрузить файл в память, распарсить его, а затем некоторые части заменить (например некоторые слова выделить жирным в HTML-тексте), после чего весь модифицированный файл сохранить.

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

AZJIO
()
Ответ на: комментарий от ugoday

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

Да, перезапуски. Лисп вообще в переди планеты всей)). В целом это легко реализуется в любом языке, хоть в том же хаскеле с использованием монад или алгебраических эффектов. Не знаю часто ли это использовалось, поскольку для узких по производительности мест это было использовать сложнее - всё в куче. Мне пользоваться не доводилось.

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

type ('a,'b) handler =
    { retc: 'a -> 'b;
      exnc: exn -> 'b;
      effc: 'c.'c t -> (('c,'b) continuation -> 'b) option }

т.е. мы можем сказать что делать если функция вернула результат (retc), если бросила исключение (exnc) и если выполнила какой-то эффект (effc).
Имея функцию вида
let int_of_string l =
  try int_of_string l with
  | Failure _ -> perform (Conversion_failure l)

let rec sum_up acc =
    let l = input_line stdin in
    acc := !acc + int_of_string l;
    sum_up acc

описав обработчик вида
 hdl = { effc = (fun (type c) (eff: c Effect.t) ->
      match eff with
      | Conversion_failure s -> Some (fun (k: (c,_) continuation) ->
              Printf.fprintf stderr "Conversion failure \"%s\"\n%!" s;
              continue k 0)
      | _ -> None
    )}

мы вызываем его так
match_with sum_up hdl

Но это всё в данный момент `unchecked exceptions` т.е. если мы не поймали что то, компилятор не предупредит нас об этом, а мы просто получим исключение в рантайме.

Вроде бы то ли в 5.7, то ли в 6.0 обещают что всё будет типизировано. Прототип в OxCaml уже работоспособен.

AndreyKl ★★★★★
()
Последнее исправление: AndreyKl (всего исправлений: 1)
Ответ на: комментарий от Waterlaz

А при чём тут величина стека в хаскеле? Я про хаскель вроде ничего не писал.

ya-betmen ★★★★★
()
Ответ на: комментарий от AndreyKl

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

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

Пенальти по производительности. Окамль

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

Теперь вспомни свой го или на чём ты там пишешь. И про количество ошибок вспомни и про возможности.

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

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

Лично я низкую популярность связываю с тем, что на других ЯП можно сделать все то же самое, но в разы проще)

goingUp ★★★★★
()
Ответ на: комментарий от goingUp

на других ЯП можно сделать все то же самое, но в разы проще

Не проще, а легче. Simple is not easy — и наоборот.

Nervous ★★★★★
()
Ответ на: комментарий от goingUp

Нервный прав, за что и получил два чая (я вообще его оппонент политический обычно, но видно тут мы сошлись мыслями, тоже бывает). Я к тебе вернусь с красивым примерчиком недельки через 2, с простым примерчиком, чтобы ты понял. Только подготовка займёт время и будет нелегко). Если, правда, на работе не загрузят сильно (на что шансов мало, честно говоря...). Если загрузят, то позднее.

AndreyKl ★★★★★
()
Последнее исправление: AndreyKl (всего исправлений: 2)
Ответ на: комментарий от goingUp

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

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

Laz ★★★★★
()
Ответ на: комментарий от goingUp

А вот если взять Хаскель, то, несмотря на то, что он компилируемый статически типизированный ЯП, он сольет по производительности скриптоте и хорошо известным тормозам типа жавы, причем сильно)

Не всегда: https://habr.com/ru/articles/490458/

Лично я низкую популярность связываю с тем, что на других ЯП можно сделать все то же самое, но в разы проще)

Не проще, а привычнее. Если сначала учиться на Хаскеле или Схеме, то потом Си/Си++/Питон жутко тормозят. Я первый свой обработчик строк на Питоне написал через def parse(s) ... parse(s[1:]), очень удивлялся, какого он так ужасно тормозит и жрёт гигабайты памяти. Приходится учитывать, что получение хвоста строки или последовательности элементов в популярных языках — офигенно дорогая операция.

monk ★★★★★
()
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.