#haskell #haskell-lens #lenses
#haskell #линза Хаскелла #линзы
Вопрос:
Недавно я поиграл с линзами и нахожу их очень приятными для использования по назначению — копания в сложных структурах данных. Но одна из областей, в которой я был бы им больше всего признателен, — это доступ к базе данных (в частности, sqlite, но я думаю, что мой вопрос распространяется на большинство баз данных), и все же я не вижу никакого способа написать линзы, которые не сильно жертвовали бы производительностью или детализацией.
Если я напишу объектив (или, я думаю, вероятно, призму, в свете полей с нулевым значением?) Из базы данных в таблицу, из таблицы в строку и из строки в столбец, каждый шаг этого влечет за собой доступ к БД, а это означает, что то, что должно быть одним доступом,минимум 4.
С другой стороны, если я стремлюсь сопоставить доступ к БД 1: 1 с использованием объектива / призмы, я получаю большие универсальные линзы, которые нельзя разбить на более мелкие части, когда я хочу просто посмотреть, какие столбцы находятся в таблице, и так далее.
Имеет ли вообще смысл использовать линзы с БД, и если да, то не упускаю ли я очевидный способ избежать дублирования работы, чтобы избежать ненужного доступа к БД?
Ответ №1:
Мне кажется, что вы хотите использовать lens аналогично linq IQueryable
в c #.
Например, если у вас есть типы:
data Project = Project {
_projectId :: Int
, _projectPriority :: Int
, _projectName :: String
, _projectTasks :: [Task]
} deriving (Show)
data Task = Task {
_taskId :: Int
, _taskName :: String
, _taskEstimate :: Int
} deriving (Show)
makeLenses ''Project
makeLenses ''Task
И база данных:
create table projects ( id, name, priority);
create table tasks (id, name, estimate, projectId);
insert into projects values (1, 'proj', 1), (2, 'another proj', 2);
insert into tasks values (1, 'task1', 30, 1), (2, 'another', 40, 1),
(3, 'task3', 20, 2), (4, 'more', 80, 2);
Если вы хотите получить список имен задач из проектов с приоритетом больше 1, было бы неплохо, если бы вы могли использовать:
highPriorityTasks :: IO [String]
highPriorityTasks = db ^.. projects . filtered (p -> p ^. projectPriority > 1 )
. projectTasks . traverse . taskName
И сделайте этот запрос к базе данных, используя запрос:
select t.name from projects as p
inner join tasks as t on t.projectId = p.id
where p.priority > 1;
К сожалению, это невозможно с библиотекой. В принципе, для эффективной работы с базой данных вам (обычно) нужно делать все в одном запросе. Это было бы неприемлемо для этого:
select * from projects where priority > 1;
for each project:
select name from tasks where projectId = <project>.id
К сожалению, невозможно разложить функции, чтобы узнать, что их создало. Помимо типа, вы ничего не можете узнать о функции, не запустив ее. Таким образом, не было бы способа извлечь данные из filtered
функции, чтобы помочь построить запрос. Также было бы невозможно извлечь вспомогательный объектив из полного выражения. Таким образом, это невозможно с использованием библиотеки lens.
Лучшее, что вы можете получить на данный момент, — это запросить базу данных, используя один набор функций, и запросить результирующие данные с помощью lens. См. Это сообщение в блоге о yesod для примера этого.
Связанный с этим вопрос заключается в том, возможно ли это вообще. Для этого нам нужно будет создать подъязык для числовых и строковых операторов и композицию, которая отслеживает, что сделано. Это может быть возможно. Например, вы можете создать тип Num, который записывает все, что с ним делается:
data TrackedNum = TrackedNum :-: TrackedNum
| TrackedNum : : TrackedNum
| TrackedNum :*: TrackedNum
| Abs TrackedNum
| Signum TrackedNum
| Value Integer
deriving (Show)
instance Num TrackedNum where
a b = a : : b
a * b = a :*: b
a - b = a :-: b
abs a = Abs a
signum a = Signum a
fromInteger = Value
t :: TrackedNum
t = 3 4 * 2 - abs (-34)
> t
(Value 3 : : (Value 4 :*: Value 2)) :-: Abs (Value 0 :-: Value 34)
Повторите процесс для логических операторов (для этого вам понадобится новый класс type), операторов списка и композиции функций (т. Е. Класса Category), и вы сможете создать функцию «белого ящика», которую затем можно использовать для создания эффективного sql-запроса. Однако это не тривиальная задача!
Комментарии:
1. Ах, это то, что я подозревал, на обоих фронтах. И да, я подумал, что это можно сделать, но, как вы сказали, сделать это адекватно нетривиально. Скорее всего, мой лучший вариант — что-то вроде Persistent, с объективами для фактической работы с данными, которые я получаю от этого. (И на самом деле, похоже, что lens поставляется с некоторыми материалами специально для Persistent, так что это похоже на подход, который считается полезным.)
2. Кажется странным, что Haskell, язык, известный своей ленью, побеждает Linq в этом конкретном направлении. Никто не понял, как составлять подобные запросы в Haskell?
3. @RobertHarvey, предложение Дэвида показывает, что это можно сделать в Haskell, просто это еще не сделано. Microsoft вкладывает миллионы долларов . Сетевая разработка, но не так много в Haskell (GHC разработан MS Research). Я уверен, что Саймону Пейтону Джонсу хотелось бы, чтобы это изменилось. 😉
4. Похоже, вам нужно проанализировать lens и другие выражения в глубоко внедренный термин DSL, чтобы вы могли выполнить необходимые преобразования для этого термина, прежде чем обозначать его в эффективный / легальный SQL. Часть этого была сделана раньше и в Haskell, хотя она еще не была выпущена для широкой публики. Аналогичные идеи были также реализованы для других областей, таких как работа Конела Эллиота по абстрагированию функциональных выражений для выражения аппаратных схем. Я не могу представить, что эта работа будет даже ужасно сложной, но это потребует времени и размышлений.
5. @Robert Harvey «Никто не понял, как составлять подобные запросы в Haskell?» Да, ищите работу HList (~ 2004) и проект Coddfish. Реляционные данные на самом деле не выигрывают от линзового подхода IMO, потому что они всегда «плоские», а не вложенные. И реляционные операторы составляют для создания другой плоской структуры. Составление запросов хорошо сочетается с функциональным подходом. Что плохо сочетается с Haskell, так это необходимость анонимных / расширяемых записей. (HList — приемлемый ответ. Сюрприз в том, что ничего лучшего не появилось совсем недавно.)