Вышла новая версия де-факто стандартного компилятора Haskell — GHC 8.2.1! Этот релиз является скорее итеративным улучшением, но вместе с тем имеет и ряд новых интересных фич, относящихся к удобству написания кода, выразительности языка и производительности скомпилированных программ. Рассмотрим же наиболее интересные, на мой взгляд, изменения!
Compact regions
Как раз одно из изменений, напрямую влияющих на производительность. Теперь можно пометить некоторый набор данных как один большой объект (region), единожды прогнать по нему сборщик мусора (compact) и считать его живым до тех пор, пока есть хотя бы одна ссылка внутрь этого региона, не залезая внутрь и не бегая по графу объектов при последующих сборках.
Это полезно, например, если программа в самом начале своей работы создаёт большой набор данных, который затем используется большую часть её последующей жизни. Например, официальное описание приводит в пример словарь для спеллчекера с выигрышем времени сборки мусора в полтора раза, а в некоторых из моих тестов время, проведённое в GC, сокращается в 2-3 раза. Папир с описанием формальной логики и реализации приводит (вероятно, на чуть более синтетических бенчмарках) вообще какие-то сумасшедшие числа (стр. 9, графики 7-8), где выигрыш иногда составляет примерно порядок, и хаскелевский GC начинает обгонять такого production-ready-монстра, как Oracle JVM с её затюненным GC.
Пользоваться этим довольно просто: для создания региона из некоторого значения предназначена функция compact :: a -> IO (Compact a)
из модуля Data.Compact
, после чего можно получить исходное (но уже «сжатое») значение через getCompact :: Compact a -> a
. Суммарно это может выглядеть как-то так:
compacted <- getCompact <$> compact someBigHeavyData
Естественно, при создании compact region'а объект вычисляется практически целиком (конкретнее — достаточно, чтобы доказать замкнутость региона), поэтому, например, компактифицировать бесконечный список — не очень хорошая идея.
Кроме того, полученный compact region можно сериализовать и десериализовать. Правда, с оговорками: десериализующая программа должна быть, в общем, точно такой же, как сериализующая, вплоть до адресного пространства, поэтому даже ASLR всё сломает.
Compactable a
, и функция compact
имеет сигнатуру Compactable a => a -> IO (Compact a)
. В реальном же API этот констрейнт отсутствует, а приписка в документации к функции говорит, что в случае наличия в регионе мутабельных данных и тому подобных некомпактифицируемых бяк будет брошено исключение. Так что, похоже, в этом случае авторы пожертвовали типобезопасностью в угоду удобству использования.
Deriving strategies
У GHC есть как минимум три с половиной механизма вывода инстансов тайпклассов:
1. Вывод стандартных классов (таких, как Show
, Read
и Eq
) и тех, которые GHC умеет выводить сам (всякие Functor
и Traversable
, а также Data
, Typeable
и Generic
).
2. Вывод через реализации методов по умолчанию, включаемый через расширение DeriveAnyClass
{-# LANGUAGE DeriveAnyClass #-}
class Foo a where
doFoo :: a -> b
doFoo = defaultImplementation
data Bar = Bar deriving(Foo)
разворачивается в
data Bar = Bar
instance Foo Bar
что полезно в случае, если Foo
может выводиться через механизм Generics (как, скажем, инстансы для конвертации в JSON у Aeson или CSV у Cassava), либо если минимальное определение Foo
не обязано иметь какие-либо методы вообще (что полезно при написании более академического кода, когда тайпкласс используется, скажем, как свидетель условия теоремы).
3. В случае алиасов типов, созданных через newtype
, также возможно использовать реализации тайпклассов для базового типа напрямую через расширение GeneralizedNewtypeDeriving
:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
newtype WrappedInt = WrappedInt { unwrap :: Int } deriving(Unbox)
Так вот, проблема в том, что до GHC 8.2 не было возможности указать, какой из механизмов должен использоваться в случае, если включено сразу несколько расширений — скажем, при одновременном включении DeriveAnyClass
и GeneralizedNewtypeDeriving
первое расширение имело приоритет, что не всегда желательно и, по факту, мешало использованию обоих расширений в одном и том же модуле.
Теперь же можно писать
{-# LANGUAGE DeriveAnyClass, GeneralizedNewtypeDeriving, DerivingStrategies #-}
newtype Baz = Baz Quux
deriving (Eq, Ord)
deriving stock (Read, Show)
deriving newtype (Num, Floating)
deriving anyclass C
Указывать стратегию можно и в standalone deriving-декларациях:
data Foo = Foo
deriving anyclass instance C Foo
Интересно, что в ранних версиях пропозала предлагалось использовать {-# прагмы #-}
, но в итоге был реализован указанный выше подход.
Другие улучшения автовывода инстансов
DeriveAnyClass
поумнел. Во-первых, теперь он не ограничен тайпклассами с сигнатурой *
либо * -> *
. Во-вторых, теперь instance constraint'ы выводятся на базе констрейнтов реализаций по умолчанию. Так, например, такой код раньше не тайпчекался:
{-# LANGUAGE DeriveAnyClass, DefaultSignatures #-}
class Foo a where
bar :: a -> String
default bar :: Show a => a -> String
bar = show
baz :: a -> a -> Bool
default baz :: Ord a => a -> a -> Bool
baz x y = compare x y == EQ
data Option a = None | Some a deriving (Eq, Ord, Show, Foo)
так как инстанс для Foo
не имел констрейнтов (Ord a, Show a)
, и компилятор предлагал добавить их руками. Теперь же соответствующие констрейнты автоматически добавляются к выводимому инстансу.
GeneralizedNewtypeDeriving
тоже поумнел. В некоторых случаях (на самом деле, в большинстве из практически интересных) ассоциированные с тайпклассом типы также выводятся автоматически. Так, например, для типов
class HasRing a where
type Ring a
newtype L1Norm a = L1Norm a deriving HasRing
компилятор сгенерирует инстанс
instance HasRing (L1Norm a) where
type Ring (L1Norm a) = Ring a
Backpack
Теперь у приверженцев OCaml чуть меньше поводов троллить хаскелистов: в GHC 8.2 появилась существенно более продвинутая система модулей (по сравнению с тем, что было раньше) — Backpack. Это само по себе довольно большое и сложное изменение, заслуживающее отдельной статьи, поэтому я просто сошлюсь на диссертацию автора реализации с формальным описанием и на более краткий пример.
Прочее
Перечислим избранные прочие изменения:
- Во внутренностях самого компилятора формализовано понятие join points — блоков кода, всегда выполняющихся после данного ветвления. Даёт несущественный, но статистически значимый прирост производительности скомпилированного кода и открывает простор для дальнейших оптимизаций.
- Улучшена производительность на NUMA-системах.
- Добавлена возможность выделять для сборщика мусора меньше потоков, чем непосредственно для
мутаторасамой программы. Simon Marlow описывает, как и зачем это было реализовано в контексте использования Haskell в Facebook в этом посте. - Улучшения в поддержке levity-полиморфизма, ответственного за возможность написания функций, работающих как с типами, обитающими в
*
(более-менее обычные типы, которые мы так любим), так и с обитающими в#
(неленивые unboxed-типы). - Улучшения в типобезопасности рефлексии.
- Возможность использовать ld.gold либо ld.lld вместо стандартного линкера ld.
- Сообщения об ошибках теперь сделаны цветными и с указателями на позицию ошибки в стиле clang.
Автор: 0xd34df00d