Вышел GHC 8.2

в 4:26, , рубрики: ghc, haskell

Вышла новая версия де-факто стандартного компилятора 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

Источник

Поделиться

* - обязательные к заполнению поля