【问题标题】:Haskell : How to cast a data type to one of its specific typeclass?Haskell:如何将数据类型转换为其特定类型类之一?
【发布时间】:2018-11-05 21:28:34
【问题描述】:

我想将 MySqlFakeClient 和 MySqlHttpClient 转换为它们的通用类型类 MySqlClient,我遇到了这个问题:

以下代码:

loadClient :: MySqlClient client => String -> client
loadClient "fake" =  MySqlFakeClient 1 -- <- It's complaining here...
loadClient "prod" =  MySqlHttpClient "http://www.google.com"
loadClient _ = error "unknown"

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

我们不能在 Haskell 中做吗?

【问题讨论】:

  • 你可能只想要data SqlClient = Http String | Fake Int; loadClient :: String -&gt; SqlClient(根本没有类型类)。

标签: haskell


【解决方案1】:

这不是 Haskell 的工作方式。像

这样的类型签名
loadClient :: MySqlClient client => String -> client

是否意味着client 可以是具有MySqlClient 实例的任何类型。这意味着(大致)调用者 可以选择具有实例MySqlClient 的类型,loadClient 将返回调用者选择的任何内容。这种类型在编译时是固定的,而您可能想要一些动态的。

对于这个例子来说,解决这个问题的方法很简单:如果你想要的是一个String,只需返回它:

loadClient "fake" = show 1
loadClient "prod" = "http://www.google.com"

现在您可能会认为“我的用例比这更复杂,这对我不起作用” - 但您可以轻松地将其扩展到更复杂的情况。基本上,您本来可以在class 中写入的任何内容都可以直接放入data 中(如果您显然不使用TypeFamilies 之类的东西,但这对于像这样的动态东西无论如何都没有意义) .

考虑例如:

data WriteBackend = WriteBackend { write :: String -> IO (), close :: IO () } 

getBackend :: String -> IO WriteBackend
getBackend ":null:" = return (WriteBackend (const $ return ()) (return()))
getBackend ":console:" = return $ WriteBackend putStrLn (return())
getBackend filename = do
  h <- openFile filename WriteMode
  return $ WriteBackend (hPutStrLn h) (hClose h)

你可以这样使用:

greetBackend :: WriteBackend -> IO ()
greetBackend b = write b "Hello World!"

main = do
  nullBackend <- getBackend ":null:"
  consoleBackend <- getBackend ":console:"
  fileBackend <- getBackend "file.txt"
  greetBackend nullBackend -- does nothing
  greetBackend consoleBackend -- writes to console
  greetBackend fileBackend -- writes to file
  close nullBackend -- does nothing
  close consoleBackend -- does nothing
  close fileBackend -- closes file

请注意,这只是在您真正需要动态方法的情况下 - 即,只要它提供正确的接口,您就不知道或不关心实际的实现是什么。如果您实际上想要区分的案例数量有限,则应该按照@Daniel Wagner 的建议使用 sum 类型。

【讨论】:

    【解决方案2】:

    另一种解决方案是使用存在类型:

    loadClient :: String -> SomeMySqlClient
    loadClient "fake" =  SomeMySqlClient $ MySqlFakeClient 1
    loadClient "prod" =  SomeMySqlClient $ MySqlHttpClient "http://www.google.com"
    loadClient _ = error
    
    data SomeMySqlClient = forall t. MySqlClient t => SomeMySqlClient t
    instance MySqlClient SomeMySqlClient where
      config (SomeMySqlClient t) = config t
    

    【讨论】:

    • 这是假设的 OOP 版本的直接等价物——具有类型类约束的存在量化值基本上是具有 vtable 的对象——但在 Haskell 中,如果你只打包一个值,它通常是矫枉过正的.
    • @JonPurdy 同意过度杀伤的说明。
    【解决方案3】:

    类型类不是面向对象命名法中的类型或超类;它们更像 C++ 中的模板,基于类型创建类似命名函数的变体。该类表示MySqlHttpClientMySqlFakeClient 都有config 函数的实例,但任何给定值仍然具有具体类型。编译器希望能够在类型分析期间在编译时解析具体类型,但是您的两种模式之间的参数区别仅在于值,而不是类型。作为同一函数的两个模式(不是不同类型类的实例),它们的类型必须匹配,并且失败。另外,我假设您的意思是默认模式中的error "unknown"undefined

    Lists of data types: "could not deduce (a ~ SomeType) from the context (SomeTypeclass a)" 显示相同的问题。

    所以我可以编写一个给定类型的loadClient 函数,假设我知道在该函数中它创建了哪个具体的 MySqlClient:

    loadClient :: MySqlClient client => String -> client
    loadClient a = case a of
        "fake" -> create "1"
        -- "prod" -> create "http://www.google.com" :: MySqlHttpClient
        _ -> undefined
    
    data MySqlHttpClient = MySqlHttpClient String
    data MySqlFakeClient = MySqlFakeClient Int
    
    class MySqlClient client where
        config :: client -> String
        create :: String -> client
    
    instance MySqlClient MySqlHttpClient where
        config (MySqlHttpClient url) = url
        create url = MySqlHttpClient url
    
    instance MySqlClient MySqlFakeClient where
        config (MySqlFakeClient myInt) = show myInt
        create num = MySqlFakeClient (read num)
    

    但这并不能解决您的困境,因为这也意味着我无法区分 create 函数,当类型变得具体时,只有其中一个是可能的。

    【讨论】:

      【解决方案4】:

      此页面:https://wiki.haskell.org/Existential_type,回答了我的问题(基本上使用存在类型,如@Rampion 建议的...):

      class Shape_ a where
         perimeter :: a -> Double
         area      :: a -> Double
      
       data Shape = forall a. Shape_ a => Shape a
      
       type Radius = Double
       type Side   = Double
      
       data Circle    = Circle    Radius
       data Rectangle = Rectangle Side Side
       data Square    = Square    Side
      
      
       instance Shape_ Circle where
         perimeter (Circle r) = 2 * pi * r
         area      (Circle r) = pi * r * r
      
       instance Shape_ Rectangle where
         perimeter (Rectangle x y) = 2*(x + y)
         area      (Rectangle x y) = x * y
      
       instance Shape_ Square where
         perimeter (Square s) = 4*s
         area      (Square s) = s*s
      
       instance Shape_ Shape where
         perimeter (Shape shape) = perimeter shape
         area      (Shape shape) = area      shape
      
      
       --
       -- Smart constructor
       --
      
       circle :: Radius -> Shape
       circle r = Shape (Circle r)
      
       rectangle :: Side -> Side -> Shape
       rectangle x y = Shape (Rectangle x y)
      
       square :: Side -> Shape
       square s = Shape (Square s)
      
       shapes :: [Shape]
       shapes = [circle 2.4, rectangle 3.1 4.4, square 2.1]
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-06-02
        • 2021-05-17
        • 1970-01-01
        • 2017-11-28
        • 2018-08-22
        • 1970-01-01
        相关资源
        最近更新 更多