List of showables: OOP beats Haskell?

后端 未结 8 1245
无人及你
无人及你 2020-12-24 06:46

I want to build a list of different things which have one property in common, namely, they could be turned into string. The object-oriented approach is straightforward: defi

相关标签:
8条回答
  • 2020-12-24 07:21

    You can store partially applied functions in the list.

    Suppose we are building a ray-tracer with different shape that you can intersect.

    data Sphere = ...
    data Triangle = ...
    
    data Ray = ...
    data IntersectionResult = ...
    
    class Intersect t where
          intersect :: t -> Ray -> Maybe IntersectionResult
    
    instance Intersect Sphere where ...
    instance Intersect Triangle where ...
    

    Now, we can partially apply the intersect to get a list of Ray -> Maybe IntersectionResult such as:

    myList :: [(Ray -> Maybe IntersectionResult)]
    myList = [intersect sphere, intersect triangle, ...]
    

    Now, if you want to get all the intersections, you can write:

    map ($ ray) myList -- or map (\f -> f ray) myList
    

    This can be extended a bit to handle an interface with multiples functions, for example, if you want to be able to get something of a shape :

    class ShapeWithSomething t where
            getSomething :: t -> OtherParam -> Float
    
    data ShapeIntersectAndSomething = ShapeIntersectAndSomething {
              intersect :: Ray -> Maybe IntersectionResult,
              getSomething :: OtherParam -> Float}
    

    Something I don't know is the overhead of this approach. We need to store the pointer to the function and the pointer to the shape and this for each function of the interface, which is a lot compared to the shared vtable usually used in OO language. I don't have any idea if GHC is able to optimize this.

    0 讨论(0)
  • 2020-12-24 07:23

    My answer is fundamentally the same as ErikR's: the type that best embodies your requirements is [String]. But I'll go a bit more into the logic that I believe justifies this answer. The key is in this quote from the question:

    [...] things which have one property in common, namely, they could be turned into string.

    Let's call this type Stringable. But now the key observation is this:

    • Stringable is isomorphic to String!

    That is, if your statement above is the whole specification of the Stringable type, then there is a pair functions with these signatures:

    toString :: Stringable -> String
    toStringable :: String -> Stringable
    

    ...such that the two functions are inverses. When two types are isomorphic, any program that uses either of the types can be rewritten in terms of the other without any change to its semantics. So Stringable doesn't let you do anything that String doesn't let you do already!

    In more concrete terms, the point is that this refactoring is guaranteed to work no matter what:

    1. At every point in your program where you turn an object into a Stringable and stick that into a [Stringable], turn the object into a String and stick that into a [String].
    2. At every point in your program that you consume a Stringable by applying toString to it, you can now eliminate the call to toString.

    Note that this argument generalizes to types more complex than Stringable, with many "methods". So for example, the type of "things that you can turn into either a String or an Int" is isomorphic to (String, Int). The type of "things that you can either turn into a String or combine them with a Foo to produce a Bar" is isomorphic to (String, Foo -> Bar). And so on. Basically, this logic leads to the "record of methods" encoding that other answers have brought up.

    I think the lesson to draw from this is the following: you need a specification richer than just "can be turned into a string" in order to justify using any of the mechanisms you brought up. So for example, if we add the requirement that Stringable values can be downcast to the original type, an existential type now perhaps becomes justifiable:

    {-# LANGUAGE GADTs #-}
    
    import Data.Typeable
    
    data Showable = Showable
        Showable :: (Show a, Typeable a) => a -> Stringable
    
    downcast :: Typeable a => Showable -> Maybe a
    downcast (Showable a) = cast a
    

    This Showable type is not isomorphic to String, because the Typeable constraint allows us to implement the downcast function that allows us to distinguish between different Showables that produce the same string. A richer version of this idea can be seen in this "shape example" Gist.

    0 讨论(0)
提交回复
热议问题