Ответ 1
Это довольно сложно, но вполне возможно с обновленной версией ghc (последняя платформа haskell будет работать). Это не так много примеров (так как все это довольно новое), поэтому я надеюсь, что это поможет вам.
Вы правы, что использование naturals уровня будет работать. Вам понадобятся два модуля: один для определения конструкций и обеспечения безопасного интерфейса, а другой - для определения фактических сервисов.
Этот код используется:
{-# LANGUAGE TypeOperators, PolyKinds, RankNTypes #-}
{-# LANGUAGE KindSignatures, DataKinds, TypeFamilies, UndecidableInstances #-}
module DefinedServices where
import ServiceTypes
import Control.Monad
apacheInstalled :: Service '[443] ServiceDetails
apacheInstalled = makeService "apache" $ putStrLn "Apache service"
torBridge :: Service [80,443] ServiceDetails
torBridge = makeService "tor" $ putStrLn "Tor service"
httpService :: Service [80, 8080] ServiceDetails
httpService = makeService "http" $ putStrLn "Http service"
serviceList1 :: [ServiceDetails]
serviceList1 = getServices $
noServices `addService` httpService `addService` apacheInstalled
-- serviceList2 :: [ServiceDetails]
-- serviceList2 = getServices $
-- noServices `addService` apacheInstalled `addService` torBridge
main = startServices serviceList1
Обратите внимание, как порты для каждой службы определены в типе. serviceList1
использует службу httpService
и apacheInstalled
. Это компилируется, поскольку их порты не конфликтуют. serviceList2
закомментирован и вызывает эту ошибку компиляции, если она не выполнена:
DefinedServices.hs:22:56:
Couldn't match type 'False with 'True
Expected type: 'True
Actual type: ServiceTypes.UniquePorts '[443, 80, 443]
In the second argument of `($)', namely
`noServices `addService` apacheInstalled `addService` torBridge'
In the expression:
getServices
$ noServices `addService` apacheInstalled `addService` torBridge
In an equation for `serviceList2':
serviceList2
= getServices
$ noServices `addService` apacheInstalled `addService` torBridge
Failed, modules loaded: ServiceTypes.
Это довольно хорошо описывает проблему: UniquePorts заканчивается как false, поскольку 443 используется дважды.
Итак, вот как это делается в ServiceTypes.hs
:
{-# LANGUAGE TypeOperators, PolyKinds, RankNTypes #-}
{-# LANGUAGE KindSignatures, DataKinds, TypeFamilies, UndecidableInstances #-}
module ServiceTypes (
makeService, noServices, addService, Service, ServiceDetails(..)
, getServices, startServices) where
import GHC.TypeLits
import Control.Monad
import Data.Type.Equality
import Data.Type.Bool
Нам нужна целая куча языковых расширений, чтобы заставить это работать. Кроме того, определен безопасный интерфейс.
Во-первых, для проверки уникальности списка требуется функция уровня уровня. Это использует операторы семейства типов в Data.Type.Equality
и Data.Type.Bool
. Обратите внимание, что следующий код выполняется только с помощью typechecker.
type family UniquePorts (list1 :: [Nat]) :: Bool
type instance UniquePorts '[] = True
type instance UniquePorts (a ': '[]) = True
type instance UniquePorts (a ': b ': rest) = Not (a == b) && UniquePorts (a ': rest) && UniquePorts (b ': rest)
Это просто рекурсивное определение единственного.
Далее, поскольку мы будем использовать сразу несколько сервисов, необходимо будет объединить два списка в один:
type family Concat (list1 :: [a]) (list2 :: [a]) :: [a]
type instance Concat '[] list2 = list2
type instance Concat (a ': rest) list2 = a ': Concat rest list2
Это все необходимые нам функции уровня уровня!
Далее я определяю тип Service
, который переносит другой тип с необходимыми портами:
data Service (ports :: [Nat]) service = Service service
Далее, для конкретных деталей одной службы. Вы должны настроить это для того, что вам нужно:
data ServiceDetails = ServiceDetails {
serviceName :: String
, runService :: IO ()
}
Я также добавил вспомогательную функцию для переноса службы в тип Service
с определенными портами:
makeService :: String -> IO () -> Service ports ServiceDetails
makeService name action = Service $ ServiceDetails name action
Теперь, наконец, для нескольких списков служб. `noServices просто определяет пустой список сервисов, который явно не использует порты:
noServices :: Service '[] [ServiceDetails]
noServices = Service []
addService
- это место, где все вместе:
addService :: (finalPorts ~ Concat ports newPorts, UniquePorts finalPorts ~ True)
=> Service ports [ServiceDetails]
-> Service newPorts ServiceDetails
-> Service finalPorts [ServiceDetails]
addService (Service serviceList) (Service newService) =
Service $ (newService : serviceList)
finalPorts ~ Concat ports newPorts
просто делает finalPorts
комбинацию портов в списке сервисов и новой службе. UniquePorts finalPorts ~ True
гарантирует, что конечные порты не содержат дубликатов портов. Остальная функция полностью тривиальна.
getServices
разворачивает [ServiceDetails]
из Service ports [ServiceDetails]
. Поскольку конструктор Service
не становится общедоступным, единственный способ создать тип Service ports [ServiceDetails]
- через функции noServices
и addService
, которые гарантированно будут безопасными.
getServices :: Service ports [ServiceDetails] -> [ServiceDetails]
getServices (Service details) = details
Наконец, функция тестирования для запуска служб:
startServices :: [ServiceDetails] -> IO ()
startServices services = forM_ services $ \service -> do
putStrLn $ "Starting service " ++ (serviceName service)
runService service
putStrLn "------"
Возможности для этой новой функциональности почти бесконечны и являются огромным улучшением по сравнению с предыдущими версиями ghc (это все еще возможно, но намного сложнее). Этот код довольно прост, как только вы начнете использовать значения в типах.