What is the preferred alternative to Fin from Idris in Haskell

前端 未结 2 2119
我在风中等你
我在风中等你 2021-02-20 06:20

I would like to have a type which can contain values 0 to n, where n lives on the type level.

I was trying something like:

import GHC.TypeLits
import Da         


        
2条回答
  •  伪装坚强ぢ
    2021-02-20 07:15

    You can directly translate Idris's Fin into the usual Haskell mishmash of sort-of-dependently-typed features.

    data Fin n where
        FZ :: Fin (S n)
        FS :: Fin n -> Fin (S n)
    
    (!) :: Vec n a -> Fin n -> a
    (x :> xs) ! FZ = x
    (x :> xs) ! (FS f) = xs ! f
    

    With TypeInType you can even have singleton Fins!

    data Finny n (f :: Fin n) where
        FZy :: Finny (S n) FZ
        FSy :: Finny n f -> Finny (S n) (FS f)
    

    This allows you to fake up dependent quantification over runtime stuff, eg,

    type family Fin2Nat n (f :: Fin n) where
        Fin2Nat (S _) FZ = Z
        Fin2Nat (S n) (FS f) = S (Fin2Nat n f)
    
    -- tighten the upper bound on a given Fin as far as possible
    tighten :: Finny n f -> Fin (S (Fin2Nat n f))
    tighten FZy = FZ
    tighten (FSy f) = FS (tighten f)
    

    but, ugh, it kinda sucks to have to duplicate everything at the value and type level, and writing out all your kind variables (n) can get pretty tedious.


    If you're really sure you need an efficient runtime representation of Fin, you can do basically what you did in your question: stuff a machine Int into a newtype and use a phantom type for its size. But the onus is on you, the library implementer, to make sure the Int fits the bound!

    newtype Fin n = Fin Int
    
    -- fake up the constructors
    fz :: Fin (S n)
    fz = Fin 0
    fs :: Fin n -> Fin (S n)
    fs (Fin n) = Fin (n+1)
    

    This version lacks real GADT constructors, so you can't manipulate type equalities using pattern matching. You have to do it yourself using unsafeCoerce. You can give clients a type-safe interface in the form of fold, but they have to be willing to write all their code in a higher-order style, and (since fold is a catamorphism) it becomes harder to look at more than one layer at a time.

    -- the unsafeCoerce calls assert that m ~ S n
    fold :: (forall n. r n -> r (S n)) -> (forall n. r (S n)) -> Fin m -> r m
    fold k z (Fin 0) = unsafeCoerce z
    fold k z (Fin n) = unsafeCoerce $ k $ fold k z (Fin (n-1))
    

    Oh, and you can't do type level computation (as we did with Fin2Nat above) with this representation of Fin, because type level Ints don't permit induction.

    For what it's worth, Idris's Fin is just as inefficient as the GADT one above. The docs contain the following caveat:

    It's probably not a good idea to use Fin for arithmetic, and they will be exceedingly inefficient at run time.

    I've heard noises about a future version of Idris being able to spot "Nat with types"-style datatypes (like Fin) and automatically erase the proofs and pack the values into machine integers, but as far as I know we're not there yet.

提交回复
热议问题