Uso adequado de typeclasses

5

Estou experimentando o módulo Gloss do Haskell, e encontrei um padrão de coisas necessárias para exibir corretamente um objeto: posição, dimensões, escala e representação de imagem. Este parecia ser um bom caso de uso de typeclasses, então escrevi isso:

class Displayable d where

    toPicture :: d -> Picture

    getDims     :: d -> (Float,Float)
    getPos      :: d -> (Float,Float)
    getScale    :: d -> Float

    setDims     :: (Float,Float) -> d -> d
    setPos      :: (Float,Float) -> d -> d
    setScale    :: Float -> d -> d

    setDims _ d     = d
    setPos _ d      = d
    setScale _ d    = d

    getDims _   = (0,0)
    getPos _    = (0,0)
    getScale _  = 0

Eu nunca usei typeclasses da alfândega antes, então tenho algumas perguntas sobre o uso deles:

  1. Em primeiro lugar, esta situação é apropriada para uma typeclass?

  2. Notei que preciso de muitos métodos para que isso funcione (provavelmente também adicionarei get/setRotation para que ele cresça). Isso é típico? Isso é um sinal de que estou tentando abranger demais?

  3. Devo definir definições padrão como eu tenho? Para determinados cenários, talvez não precise definir uma característica específica, mas gostaria que o objeto aproveitasse o restante da classe (como a necessidade de girar um mundo ou obter as dimensões de uma "entidade não física"). No entanto, percebo que, se as definições padrão não forem consideradas, isso pode levar a alguns resultados estranhos (como objetos que têm uma escala 0 por padrão, o que pode torná-los invisíveis).

  4. Os getters / setters são a melhor maneira de alcançar o que estou tentando fazer (uma interface central para manipular objetos exibíveis)?

(Eu sei que as representações de dados não são consideradas objetos em Haskell, mas dado que estou tentando representar algo visual, achei que o termo objeto seria apropriado)

Quaisquer pensamentos seriam apreciados.

    
por Carcigenicate 05.11.2014 / 02:52
fonte

2 respostas

4

Normalmente, nesse tipo de situação, você criaria um tipo de dados simples ou talvez um registro para agregar os diferentes campos:

data Displayable = Displayable Picture (Float,Float) (Float,Float) Float

As classes de tipo são para quando você precisa de um envio polimórfico para muitas implementações da mesma função. Esse certamente não é o caso de escala e posição, para o qual você terá apenas uma implementação. As dimensões podem ter formas diferentes de serem calculadas, como as dimensões naturais de um bitmap, mas essas diferenças quase certamente podem ser contabilizadas no momento da construção.

Você pode ter um caso para tornar toPicture parte de uma classe de tipo, mas isso provavelmente pode ser tratado principalmente em tempo de construção, e então ter uma função que adiciona as transformações apropriadas sobre uma base Picture apenas antes de renderizá-lo.

Outra possível estrutura de dados pode ser uma Picture e uma lista de funções de transformação para aplicá-la:

data Displayable = Displayable Picture [Picture -> Picture]

Isso torna mais fácil adicionar novos tipos de transformações, como rotações ou até mesmo combinações complexas de transformações, mas torna mais difícil fazer operações como redefinir a escala de volta ao padrão. Talvez você possa adicionar uma descrição String para cada transformação. Qual é o melhor vai depender do seu aplicativo. Meu ponto em incluir este exemplo é mostrar que o uso de campos de função pode impedir que você recorra a uma classe de tipos.

Não é que as classes de tipos não sejam úteis. Longe disso. É só que as situações em que criar uma nova é a melhor opção são relativamente raras. Quando você precisa criá-los, eles geralmente são muito pequenos e quase nunca mudam. Se você acha que pode precisar adicionar uma função a uma classe de tipo mais tarde, isso deve fazer você pensar duas vezes antes de usar uma.

Não há problema em ter muitas funções para manipular o tipo de dados de maneira mais amigável ao programador, mas você quer que elas estejam fora das classes de tipo, se possível. Toda vez que você adiciona uma função a uma classe de texto, você precisa adicioná-la a todas as instâncias.

    
por 05.11.2014 / 05:18
fonte
3

Geralmente é uma boa ideia evitar classes de tipos, a menos que forneça um benefício específico que não possa ser obtido por nenhum outro meio. Este artigo fornece uma visão um pouco extrema sobre isso, então leve-o com um grão de sal, mas pode ajudá-lo a entender por que as classes de tipos podem ser problemáticas.

As classes de tipo de problema principal operam no nível de tipo , o que significa que tentar fazer coisas complicadas com classes de tipo normalmente exigirá uma infinidade de extensões Haskell e, às vezes, até mesmo impossível. em contraste, operar no nível de valor é muito mais simples e você não precisa lutar com o verificador de tipos e digitar inferencer regularmente.

Os getters e setters têm seus lugares em Haskell, mas eles são principalmente para manter a compatibilidade da interface e usá-los excessivamente causará muito clichê. Para estruturas de dados internas simples, elas podem não valer o esforço extra.

Em seu exemplo, seria muito mais simples implementar Displayable como um tipo de dados comum (como mostrado no exemplo de Karl Bielefeldt), com o benefício adicional de poder armazenar todo o seu Displayables em um contêiner sem extensões funky como ExistentialTypes . Além disso, você pode usar a correspondência de padrões (ou a sintaxe do registro) para desconstruir seu Displayables .

Na questão das definições padrão, elas devem ser usadas com cautela. Além disso, provavelmente é uma má idéia escrever padrões que não funcionam em todas as instâncias. Não é uma diretriz estrita, mas realmente ajuda a evitar erros bobos como esquecer de escrever uma função para uma instância: sem padrões, se você esquecer alguma coisa, o compilador lhe dará um aviso, ou na pior das hipóteses causará um erro de tempo de execução; esse não é o caso se ele é padronizado, e se o comportamento padrão é errado , então seu programa pode acabar sendo sutilmente quebrado.

Adendo: um "registro de métodos" (ou "dicionário de instâncias", como é frequentemente chamado) é, na verdade, o que o Haskell usa para implementar classes de tipos embaixo do tapete. Tal abordagem seria algo como:

data Displayable d = Displayable
  { display   :: d
  , toPicture :: d -> Picture
  , getDims   :: d -> (Float, Float)
  , getPos    :: d -> (Float, Float)
  , getScale  :: d -> Float
  , setDims   :: (Float,Float) -> d -> d
  , setPos    :: (Float,Float) -> d -> d
  , setScale  :: Float -> d -> d
  }

Qualquer classe de tipo pode ser transformada neste formulário: uma tradução literal da classe de tipo em sua pergunta. Você provavelmente não desejará usá-lo desta forma exata: é melhor descobrir quais partes do Displayable realmente precisam ser abstratas e quais partes podem ser concretas . Muita flexibilidade pode tornar o código mais complexo do que precisa ser.

    
por 05.11.2014 / 05:21
fonte