Как я могу абстрагировать команду / ответ расширяемым образом? - PullRequest
5 голосов
/ 08 мая 2020

Пытаясь создать дизайн на основе домена в Haskell, я столкнулся с этой проблемой:

data FetchAccessories = FetchAccessories
data AccessoriesResponse = AccessoriesResponse

data FetchProducts = FetchProducts
data ProductsResponse = ProductsResponse

type AccessoryHandler = FetchAccessories -> AccessoriesResponse
type ProductHandler = FetchProducts -> ProductsResponse

handle :: Handler -- Not sure how to do this abstraction
handle FetchAccessories = AccessoriesResponse
handle FetchProducts = ProductsResponse

someFn :: AccessoriesResponse  -- Ideally
someFn = handle FetchAccessories

Я бы хотел ie вместе с одним Fetch * с один ответ * и предоставит достаточно информации, чтобы компилятор знал, если я вызываю handle FetchAccessories, он может вернуть только AccessoriesResponse.

Изменить:

Еще более идеально, это сработает без аннотации, продукты и аксессуары с предполагаемым соответствующим типом:

biggerFn =
  let products = handle FetchProducts
      accessories = handle FetchAccessories
  in
    undefined -- do business things 

Ответы [ 2 ]

6 голосов
/ 08 мая 2020

Как указано, это как раз работа для классов типов. Типовой класс - это конструкция, которая позволяет вам объединить ie несколько типов, а также определить некоторые функции для этих типов.

В вашем случае мы можем определить класс Handler следующим образом:

class Handler request response where
    handle :: request -> response

Здесь request и response - это переменные типа, представляющие два типа, которые «связываются вместе» нашим классом, и функция handle, которая принимает один и возвращает другой.

Затем мы можем определить экземпляры этого класса для ваших двух случаев:

instance Handler FetchAccessories AccessoriesResponse where
    handle FetchAccessories = AccessoriesResponse

instance Handler FetchProducts ProductsResponse where
    handle FetchProducts = ProductsResponse

И затем мы можем использовать функцию:

someFn :: AccessoriesResponse
someFn = handle FetchAccessories

(обратите внимание, что вы необходимо включить MultiParamTypeClasses, чтобы это работало)


В ответ на ваш комментарий: Интересно, есть ли способ избежать аннотации в someFn (поскольку gh c кажется очень близким к знанию)

Проблема в том, что GH C на самом деле не близок к знанию. Вы знаете, что AccessoriesResponse идет только с FetchAccessories, но что касается GH C, это не обязательно так. В конце концов, вы можете go вперед и добавить еще один экземпляр класса, например:

instance Handler FetchAccessories String where
    handler FetchAccessories = "foo"

И теперь оказывается, что handle FetchAccessories может означать либо AccessoriesResponse, либо "foo". GH C не может решить за вас.

Но вы можете явно сказать, что для каждого типа запроса может быть только один тип ответа. Это называется «функциональной зависимостью» (для этого необходимо включить FunctionalDependencies), а его синтаксис следующий:

class Handler request response | request -> response where
    handler :: request -> response

Это сообщит GH C, что response однозначно определяется request и будет иметь два практических последствия: (1) GH C отклонит второй экземпляр Handler FetchAccessories String из моего примера выше, жалуясь, что он нарушает функциональную зависимость, и (2) GH C сможет выяснить, что такое response, просто зная request.

В частности, это означает, что вы можете опустить подпись типа на someFn.

В соответствующем примечании вы можете или может не захотеть делать противоположное: на каждый ответ может быть только один запрос. Для этого вы можете указать две функциональные зависимости:

class Handler request response | request -> response, response -> request where

(приведенное ниже больше не актуально в свете вашего комментария, но я оставлю это здесь для протокола)

Однако я подозреваю , что на самом деле вы имели в виду модель другого типа. Я подозреваю, что вы имели в виду модель, в которой говорится: « запрос может быть либо для продуктов, либо для аксессуаров, а ответ может быть либо для продуктов, либо для аксессуаров, а функция handle превратит любой запрос в соответствующий ответ"

Если это действительно то, что вы действительно имели в виду, подходящей моделью будут типы сумм:

data Fetch = FetchAccessories | FetchProducts
data Response = AccessoriesResponse | ProductsResponse

handle :: Fetch -> Response
handle FetchAccessories = AccessoriesResponse
handle FetchProducts = ProductsResponse
0 голосов
/ 14 мая 2020

Если у вас есть закрытый набор выборок, это также может быть естественным вариантом использования для GADT, индексированного тегом, который указывает тип запроса или ответа.

{-# LANGUAGE DataKinds, GADTs, KindSignatures, StandaloneDeriving #-}

data Tag = Accessories | Products

data Fetch (t :: Tag) where
  FetchAccessories :: Fetch 'Accessories
  FetchProducts :: Fetch 'Products

deriving instance Show (Fetch t)

data Response (t :: Tag) where
  AccessoriesResponse :: Response 'Accessories
  ProductsResponse :: Response 'Products

deriving instance Show (Response t)

type Handler t = Fetch t -> Response t

-- Handles any type of request.
handle :: Handler t
handle FetchAccessories = AccessoriesResponse
handle FetchProducts = ProductsResponse

-- Handles only accessory requests.
-- The compiler knows this matching is exhaustive.
handleAccessories :: Handler 'Accessories
handleAccessories FetchAccessories = AccessoriesResponse

someFn :: Response 'Accessories
someFn = handle FetchAccessories

Конечно, вы можете добавить дополнительные поля к типам Fetch и Response.

data Fetch (t :: Tag) where
  FetchAccessories :: AccessoryName -> Fetch 'Accessories
  FetchProducts :: ProductId -> Fetch 'Products
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...