Расшифровка сообщений об ошибках объектива, часть 1

Знаменитая библиотека Haskell lens, на мой взгляд, является одним из его преимуществ как языка. Это дает возможность получать доступ, изменять и запрашивать глубоко вложенные структуры данных с эффективностью, которая дает прямой доступ к свойствам в стиле ООП foo.bar.baz = “qux” с точками и прямо в нирвану программирования.

Однако научиться использовать lens может быть нелегко, хотя существует множество отличных вводных руководств (1, 2, 3), а автор библиотеки даже дал впечатляющее ( и впечатляюще доступный) поговорим о библиотеке , все эти ресурсы сосредоточены на том, что делать, когда все работает хорошо. Это нормально, но, особенно для новичков, программирование гораздо чаще состоит из того, что что-то идет не так неправильно. Lens, в частности, значительно усложняет работу с этим, поскольку из-за техники, которую он использует, чтобы все хорошо скомпоновано, сообщения об ошибках, которые выдает GHC, оставляют желать лучшего. Этот пост является первым в продолжающейся серии о расшифровке и декодировании сообщений об ошибках, которые GHC выдает нам при неправильном использовании линз.

Этот объектив здесь не работает!

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

Например, у нас есть следующий код:

Поскольку мы использовали макросы haskell шаблона makeFields для определения наших линз по типам данных Galaxy и Human (вместо makeLenses или определения их вручную как линз верхнего уровня), это на самом деле выглядит довольно красиво. Если мы попытаемся получить доступ к полю species на Human, например, мы получим следующее:

Это связано с тем, что макрос makeFields определяет классы типов для линз, которые он определяет, в форме HasThing, т. Е. Для нашего поля Name (которое используется как Human, так и Galaxy), мы получим что-то вроде:

Два параметра HasName класса типов позволяют различным типам данных иметь поле name с разными типами, что приятно.

Но что, если нам не нужны классы типов для полей линз? Или что, если мы (очень разумно) не хотим использовать шаблон haskell и вместо этого напишем все линзы вручную? Например, поле nickname типа Animal генерируется makeLenses, а не makeFields, что дает нам что-то вроде:

Если теперь мы попытаемся использовать поле nickname на человеке, мы получим следующее, более запутанное сообщение об ошибке:

Хотя первая строка сообщения об ошибке довольно ясна, ясность быстро падает - что такое Getting? И почему OrganismName появляется перед Animal в функции после него?

Не вдаваясь в подробности (я бы рекомендовал прочитать страницу создания линз или даже сообщение в блоге, в котором изначально был изобретен способ работы линз в настоящее время, если вам больше интересно), это связано с тем, как линзы Библиотека использует уловку с функторами, чтобы обеспечить такую ​​композицию, которую мы обычно ожидаем от библиотеки линз. Он кодирует (мономорфные) линзы как:

а затем меняет местами функтор f, чтобы получить поведение геттера / сеттера - Const дает нам Getter, а Identity дает нам сеттер.

Итак, когда мы видим следующую функцию в нашем сообщении об ошибке:

На самом деле GHC говорит, что мы поставили Getter с Animal по OrganismName! Как правило, каждый раз, когда вы видите сообщение об ошибке следующего типа:

вы можете предположить, что GHC говорит о Getter с source по target! И всякий раз, когда вы видите Getting target source target, вы можете прочитать это как «Получение target от source».

Подведение итогов

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

Приходите поговорите с нами, если работа с линзами и Haskell в целом вызывает у вас такое же воодушевление, как и у меня и других инженеров в Urbint.