scooter me fecit
scooter me fecit

Reputation: 1063

Type classes. families and polymorphism

I'm trying to write a fairly straightforward processor emulator and am trying to separate the actual processor from its memory interface. For example, an old TRS-80 has 12K of ROM and can be configured with a total of 16, 32 or 48K of total RAM. My initial idea for the memory system was a type class:

class MemorySystem memInternals addrType wordType where
  -- | General memory fetch. This always returns a 'wordType' value,
  -- even if the system's memory internals are byte addressable.
  mfetch :: addrType       -- ^ The address from which the instruction will be fetched
         -> memInternals   -- ^ The memory system
         -> wordType       -- ^ The fetched word

  -- | Fetch a block of words from the memory system
  mfetchN :: ( Unbox wordType
             ) =>
             addrType      -- ^ The address from which the instruction will be fetched
          -> Int           -- ^ Number of words to fetch
          -> memInternals  -- ^ The memory system
          -> Vector wordType -- ^ The words fetched

Clarification: the point of MemorySystem is to allow an instruction decoder, e.g., a Z80 instruction decoder in the case of a TRS-80, to operate independently of its memory interface, such that all the decoder need do is call mfetch for the next instruction and its operands. Hence, memInternals is a type that implements an interface to a memory system specified partially by the type class functions.

How is the best way of constraining addrType and wordType while allowing memInterals to be polymorphic, i.e., allowing memInternals to represent different memory configurations. I'm finding myself adding contexts to functions that look like:

foo :: ( MemorySystem memSys Word16 Word8) =>
    -> memSys
    -> Word16
    -> Word16
bar :: ( MemorySystem memSys Word16 Word8) =>
    -> memSys
    -> Word16
    -> Word8
bar mem pc = ...
  -- bar calls foo -> rigid type variable error...

which leads to a lot of rigid type variable errors in ghc.

Is there a better way of expressing a MemorySystem that leads to the "right type" of polymorphism in memInternals such that a family of memory interfaces can be properly represented (and acted upon via a type class or family)?

Upvotes: 1

Views: 181

Answers (2)

scooter me fecit
scooter me fecit

Reputation: 1063

The record approach does work best, at least for proper separation of concerns. Here's what works for me:

data (Unbox wordType) => MemorySystem addrType wordType memSys =
  MemorySystem
  { _mfetch :: addrType -> wordType
  , _ mfetchN :: addrType -> Int -> Vector wordType
  , _memInternals = memSys
  }

(Note that memInternals can be accessed from within mfetch and mfetchN, so no need to duplicate it as a parameter to those two functions.) It's then possible to create lenses for this record, e.g., using Control.Lens and makeLenses. It also permits defining types like:

import Data.Word
import Data.Int

type Z80addr = Word16
type Z80word = Word8
type Z80memory memConfig = MemorySystem Z80addr Z80word memConfig

that permit a polymorphic memory configuration. Many thanks to Chris for the suggestion -- I'm not sure why I got wrapped around the proverbial axle thinking in terms of type families.

Upvotes: 0

Chris Kuklewicz
Chris Kuklewicz

Reputation: 8153

To expand on my comment, you can try using associated type families. But it looks like your current use case is well served by the data type instead of the type class:

{-# LANGUAGE TypeFamilies, RankNTypes #-}
import Data.Vector.Unboxed

class MemorySystem memInternals where
  type AddrType memInternals
  type WordType memInternals

  mfetch :: memInternals
         -> AddrType memInternals
         -> AddrType wordType

  mfetchN :: Unbox wordType
          => memInternals
          -> AddrType memInternals
          -> Int
          -> Vector wordType

-- The record type may be more flexible and appropriate here
data MemorySystem addrType wordType = MemorySystem {
    mfetch :: addrType -> wordType
  , mfetchN :: Unbox wordType => addrType -> Int -> Vector wordType
  }

See how the type class has only functions that take the type parameter memInternals only once and as the first argument? This seems equivalent to the record approach.

Upvotes: 1

Related Questions