Haskell技术疑问:如何将具体类型转换为公共类型类MySqlClient?
首先,你的代码报错的核心原因是Haskell的参数化多态和你期望的“根据输入返回不同具体类型”的需求不匹配。
你写的loadClient类型签名MySqlClient client => String -> client表达的是:调用者可以指定任意一个实现了MySqlClient的类型client,函数都要返回该类型的值。但显然你的函数做不到这一点——当输入是"fake"时,你只能返回MySqlFakeClient,不可能返回MySqlHttpClient,所以编译器会报错,因为它无法证明MySqlFakeClient 1能满足任意的client类型约束。
那怎么实现“根据输入返回不同的、都实现了MySqlClient的类型”呢?下面是几种常见的解决方案:
方案一:用代数数据类型(Sum Type)包装具体客户端
这是最直观的方式,创建一个新的类型来包裹所有可能的客户端实现,然后为这个包裹类型实现MySqlClient接口:
-- 保留你原来的定义 data MySqlHttpClient = MySqlHttpClient String data MySqlFakeClient = MySqlFakeClient Int class MySqlClient client where config :: client -> String instance MySqlClient MySqlHttpClient where config (MySqlHttpClient url) = url instance MySqlClient MySqlFakeClient where config (MySqlFakeClient myInt) = show myInt -- 新增包裹类型 data MySqlClientWrapper = WrapFake MySqlFakeClient | WrapHttp MySqlHttpClient -- 为包裹类型实现MySqlClient instance MySqlClient MySqlClientWrapper where config (WrapFake c) = config c config (WrapHttp c) = config c -- 修改loadClient的返回类型为包裹类型 loadClient :: String -> MySqlClientWrapper loadClient "fake" = WrapFake (MySqlFakeClient 1) loadClient "prod" = WrapHttp (MySqlHttpClient "http://www.google.com") loadClient _ = error "unknown client type"
现在loadClient返回的是一个具体的MySqlClientWrapper类型,你可以直接对它调用config函数,它会根据内部包裹的实际类型转发调用。
方案二:使用存在类型(Existential Types)
如果你不想手动写包裹类型的每个分支,可以使用存在类型,需要启用ExistentialQuantification扩展:
{-# LANGUAGE ExistentialQuantification #-} data MySqlHttpClient = MySqlHttpClient String data MySqlFakeClient = MySqlFakeClient Int class MySqlClient client where config :: client -> String instance MySqlClient MySqlHttpClient where config (MySqlHttpClient url) = url instance MySqlClient MySqlFakeClient where config (MySqlFakeClient myInt) = show myInt -- 定义存在类型,包裹任何实现了MySqlClient的类型 data AnyMySqlClient = forall client. MySqlClient client => AnyMySqlClient client -- 为存在类型实现MySqlClient instance MySqlClient AnyMySqlClient where config (AnyMySqlClient c) = config c -- 修改loadClient返回存在类型 loadClient :: String -> AnyMySqlClient loadClient "fake" = AnyMySqlClient (MySqlFakeClient 1) loadClient "prod" = AnyMySqlClient (MySqlHttpClient "http://www.google.com") loadClient _ = error "unknown client type"
AnyMySqlClient是一个“存在容器”,它可以容纳任何实现了MySqlClient的类型,同时对外暴露MySqlClient的接口。
方案三:使用GADT(广义代数数据类型)
GADT是存在类型的一种更灵活的写法,需要启用GADTs扩展:
{-# LANGUAGE GADTs #-} data MySqlHttpClient = MySqlHttpClient String data MySqlFakeClient = MySqlFakeClient Int class MySqlClient client where config :: client -> String instance MySqlClient MySqlHttpClient where config (MySqlHttpClient url) = url instance MySqlClient MySqlFakeClient where config (MySqlFakeClient myInt) = show myInt -- 用GADT定义容器 data MySqlClientGADT where MySqlClientGADT :: MySqlClient client => client -> MySqlClientGADT -- 为GADT实现MySqlClient instance MySqlClient MySqlClientGADT where config (MySqlClientGADT c) = config c loadClient :: String -> MySqlClientGADT loadClient "fake" = MySqlClientGADT (MySqlFakeClient 1) loadClient "prod" = MySqlClientGADT (MySqlHttpClient "http://www.google.com") loadClient _ = error "unknown client type"
这个方案和存在类型的效果类似,但GADT的写法更灵活,后续如果需要扩展接口也更方便。
内容的提问来源于stack exchange,提问作者Nicolas Henin

