Высокопроизводительный драйвер PostgreSQL для Haskell.
Компилируемый чисто функциональный язык с сильной типизацией, статической проверкой и автоматическим выведением типов.
Удовлетворяет двум требованиям:
Удовлетворяет двум требованиям:
Удовлетворяет двум требованиям:
При повторном её исполнении с теми же параметрами она всегда возращает тот же результат. (Референциальная прозрачность).
Исполнение функции не оказывает какого-либо воздействия на состояние программы и не осуществляет взаимодействия с внешним миром.
Удовлетворяет двум требованиям:
При повторном её исполнении с теми же параметрами она всегда возращает тот же результат. (Референциальная прозрачность).
Исполнение функции не оказывает какого-либо воздействия на состояние программы и не осуществляет взаимодействия с внешним миром. (Отсутствие сторонних эффектов).
Java:
public static String exclamation ( String phrase ) {
return phrase + "!";
}
Haskell:
exclamation :: String -> String
exclamation phrase = phrase <> "!"
Референциально непрозрачная функция:
public static String exclamation ( String phrase ) {
return phrase + generateRandomString() + "!";
}
Функция со сторонними эффектами:
public static String exclamation ( String phrase ) {
launchRockets();
return phrase + "!";
}
Предсказуемость
Возможность использования и тестирования в полной изоляции
В большинстве языков любая функция может быть грязной и это даже никаким образом не отображается в типах.
В Haskell все функции чистые!
public static <A> Integer listLength ( List<A> as )
Метод listLength
и тип List
можно называть полиморфными, потому что они параметризованы абстрактным типом A
.
public static <A> Integer listLength ( List<A> as )
Метод listLength
и тип List
можно называть полиморфными, потому что они параметризованы абстрактным типом A
.
Для сведения, в Haskell аналогичное объявление выглядело бы так:
listLength :: List a -> Int
Tuple2<A, B>
(a, b)
Для сведения, объявить эти типы можно было бы как-то так:
public class Tuple2<A, B> {
public final A _1;
public final B _2;
// Ridiculous Java boilerplate excluded
}
data (,) a b = (,) a b
Tuple2<Integer, String>
(Int, String)
Tuple3<Integer, String, Integer>
(Int, String, Int)
(Int, String)
- кортеж, объединяющий 2 элемента: Int
и String
.
(Int, Int, Double)
- кортеж, объединяющий три элемента.
...
()
- тип данных, имеющий единственное значение ()
. На словах называется "Unit".
Думать о нём можно как о кортеже из нуля элементов.
Используется для указания отсутствия значения.
Ведь все функции в Haskell оперируют значениями, референциально прозрачны и не производят сторонних эффектов...
Решение - завернуть взаимодействие с внешним миром в отдельный тип данных:
IO a
IO a
- описание действия с внешним миром, возвращающего результат типа a
.
putStrLn :: String -> IO ()
readFile :: FilePath -> IO String
Имея следующие функции:
putStrLn :: String -> IO ()
readFile :: FilePath -> IO String
(>>=) :: IO a -> (a -> IO b) -> IO b -- "flatMap" anybody?
Можем их скомбинировать так:
outputFile :: FilePath -> IO ()
outputFile filePath =
readFile filePath >>= putStrLn
outputFile :: FilePath -> IO ()
outputFile filePath =
readFile filePath >>= putStrLn
Идентично следующему объявлению с помощью нотации "do":
outputFile :: FilePath -> IO ()
outputFile filePath =
do
contents <- readFile filePath
putStrLn contents
outputTwoFiles :: FilePath -> FilePath -> IO ()
outputTwoFiles filePath1 filePath2 =
do
contents1 <- readFile filePath1
contents2 <- readFile filePath2
putStrLn (contents1 <> "\n" <> contents2)
Напоминает императивный код, не так ли?
Однако это, по-прежнему, чистая функция из двух значений FilePath
в значение IO ()
. Напоминаю, что, в отличие от императивных языков, функция сама никаких обращений к файловой системе не делает, а лишь создаёт спецификацию того, что нужно сделать.
Вот такая функция простая?
execParams :: Connection -- connection
-> ByteString -- statement
-> [Maybe (Oid, ByteString, Format)] -- parameters
-> Format -- result format
-> IO (Maybe Result) -- result
Вот такая функция простая?
execParams :: Connection -- connection
-> ByteString -- statement
-> [Maybe (Oid, ByteString, Format)] -- parameters
-> Format -- result format
-> IO (Maybe Result) -- result
Hell no!
Количество вещей, о которых нам надо заботиться.
Сокращать количество проблем, которые нам надо учитывать в каждом случае.
"Боже, а что если что-то случится между данными выражениями?"
"А что, если программа находится в неожидаемом состоянии?"
"А что, если произойдёт сбой или где-нибудь выкинется исключение?"
"Боже, а что если что-то случится между данными выражениями?"
"А что, если программа находится в неожидаемом состоянии?"
"А что, если произойдёт сбой или где-нибудь выкинется исключение?"
Haskell позволяет чётко ограничивать код, взаимодействующий с внешним миром.
Код, не взаимодействующий с внешним миром, вообще не сталкивается с упомянутой категорией проблем, и это прекрасно!
Минимизировать и изолировать IO.
Чрезмерные абстракции или страдают от недостатка возможностей, или "протекают".
Автор абстрагировался над какими-то действиями, а потом окольными путями пытается вернуть потерянный функционал.
Например, Hook в типичном ORM.
Результат - концептуальная каша.
Абстрагироваться насколько возможно, но дисциплинированно.
Задача - найти общие черты, не теряя в возможностях.
Пойдём от общего к частному.
Нет непосредственного соответствия для базы данных (схемы)
Нет непосредственного соответствия для отношений
Нет непосредственного соответствия для базы данных (схемы)
Нет непосредственного соответствия для отношений
Нет непосредственного соответствия для таблиц
Нет непосредственного соответствия для базы данных (схемы)
Нет непосредственного соответствия для отношений
Нет непосредственного соответствия для таблиц
Нет непосредственного соответствия даже для строк
Нет непосредственного соответствия для базы данных (схемы)
Нет непосредственного соответствия для отношений
Нет непосредственного соответствия для таблиц
Нет непосредственного соответствия даже для строк
Всё то же касается и любого OO-языка. Именно поэтому ORM заведомо обречены либо быть серьёзно урезанными в возможностях, либо быть текущей абстракцией!
BIGINT
от Postgres непосредственно соответствует Int64
в Haskell
NUMERIC
- Scientific
TIMESTAMPTZ
- UTCTime
Вообще, это распространяется на все примитивные значения.
Не проблема! Соответсвуют Vector
, []
или любым другим данным, которые можно свёртывать и развёртывать (fold).
Многоуровневые массивы не составляют исключения.
Тоже не проблема. У Haskell есть кортежи и пользовательские типы.
Ну, вы поняли. Всё есть.
Всё, что нужно клиенту базы данных:
Всё, что нужно клиенту базы данных:
Всё, что нужно клиенту базы данных:
Поддерживать соединение с БД
Исполнять SQL
Всё, что нужно клиенту базы данных:
Поддерживать соединение с БД
Исполнять SQL
Всё остальное может быть декларативным.
Взаимодействие с внешним миром в Hasql осуществляется следующими функциями:
Взаимодействие с внешним миром в Hasql осуществляется следующими функциями:
Hasql.Connection.acquire
- установить соединениеВзаимодействие с внешним миром в Hasql осуществляется следующими функциями:
Hasql.Connection.acquire
- установить соединение
Hasql.Connection.release
- закрыть соединение
Взаимодействие с внешним миром в Hasql осуществляется следующими функциями:
Hasql.Connection.acquire
- установить соединение
Hasql.Connection.release
- закрыть соединение
Hasql.Session.run
- взаимодействовать с базой данных
Взаимодействие с внешним миром в Hasql осуществляется следующими функциями:
Hasql.Connection.acquire
- установить соединение
Hasql.Connection.release
- закрыть соединение
Hasql.Session.run
- взаимодействовать с базой данных
Всё.
Чего несёт с собой следующая информация?
SELECT name, birthday
FROM person
WHERE birthday >= $1 AND gender = $2
Чего несёт с собой следующая информация?
SELECT name, birthday
FROM person
WHERE birthday >= $1 AND gender = $2
Что это строчное значение, напоминающее SQL-выражение, содержащее параметры $1
и $2
.
Чего несёт с собой следующая информация?
SELECT name, birthday
FROM person
WHERE birthday >= $1 AND gender = $2
Если типизировать, то это String
, например.
Чего несёт с собой следующая информация?
SELECT name, birthday
FROM person
WHERE birthday >= $1 AND gender = $2
Если типизировать, то это String
, например.
Маловато будет!
Чего несёт с собой следующая информация?
SELECT name, birthday
FROM person
WHERE birthday >= $1 AND gender = $2
А что, если тип был бы таким:
Query (Day, Gender) (Vector (Text, Day))
-- ^ Параметры ^ Результат
Haskell:
Query (Day, Gender) (Vector (Text, Day))
Java:
Query<Tuple2<Date, Gender>, ArrayList<Tuple2<String, Date>>>
Query
- полиморфный тип следующего вида:
Haskell:
Query parameters result
Java:
Query<Parameters, Result>
Объявить Query
можно при помощи этой функции:
statement :: ByteString -- The SQL
-> Encoders.Params a -- The parameters encoder
-> Decoders.Result b -- The result-set decoder
-> Bool -- The "prepared" flag
-> Query a b
Пример объявления Query
:
selectOfPersonsBornAfter :: Query Day (Vector (Text, Day))
selectOfPersonsBornAfter = statement sql encoder decoder True
where
sql = "SELECT name, birthday FROM person WHERE birthday >= $1"
encoder = Encoders.value Encoders.date
decoder = Decoders.rowsVector row
where
row = liftA2 (,) name birthday
where
name = Decoders.value Decoders.text
birthday = Decoders.value Decoders.date
Итак, Query
- это полностью энкапсулированная, не протекающая и не ограничивающая нас ни в чём абстракция над рядом связанных проблем:
Итак, Query
- это полностью энкапсулированная, не протекающая и не ограничивающая нас ни в чём абстракция над рядом связанных проблем:
Спросите: "И чего такого?" Да то, что после того, как Query
написан, ни одна из этих проблем Вас больше не волнует!
А ещё!..
Query
является Профунктором, что, несомненно, доставляет!
Полностью декларативный и изолированный от сторонних эффектов!
Комбинируемый и гибкий! Поддерживает данные любой сложности.
Максимально производительный благодаря непосредственному использованию ресурсов и бинарного формата передачи данных.
Транзакции не комбинируемы. Невозможно взять две транзакции и объединить в одну.
Обработка конфликтов и откаты не дружат со сторонними эффектами.
Длительные операции на стороне клиента в контексте транзакции удерживают замки на БД, что никогда не есть хорошо.
Решение - сделать транзакции чище, ограничив допустимые действия в контексте транзакции только на общение с БД, на которой выполняется транзакция.
Иными словами, никаких side-effects, вроде обращения по сети куда-то или записи в файл, мутации состояния чего-либо в программе и тп.
В итоге, получаем следующую абстракцию:
Transaction a
Которая умеет выполнять только следующие две операции:
sql :: ByteString -> Transaction ()
-- Исполняет непараметризованный SQL,
-- который может содержать множество стейтментов.
-- Ничего при этом не возвращает.
query :: a -> Query a b -> Transaction b
-- Исполняет Query, передавая ему параметры.
-- Возвращает результат Query.
Эту абстракцию можно комбинировать тем же способом, что и IO:
transferMoneyTransaction :: Int -> Int -> Scientific -> Transaction ()
transferMoneyTransaction accountID1 accountID2 amount =
do
query (accountID1, amount) takeMoneyQuery
query (accountID2, amount) putMoneyQuery
transferMoneyTwiceTransaction :: Int -> Int -> Scientific -> Transaction ()
transferMoneyTwiceTransaction accountID1 accountID2 amount =
do
transferMoneyTransaction accountID1 accountID2 amount
transferMoneyTransaction accountID1 accountID2 amount
Заявленные свойства позволяют автоматически откатывать и перезапускать транзакции в случае конфликтов. Так это и происходит.
Данная абстракция представлена в библиотеке "hasql-transaction".
Бьёт всех конкурентов в 2 раза и более!
Старые бенчмарки:
https://nikita-volkov.github.io/hasql-benchmarks/