2017-08-30 5 views
0

Examiner les dossiers et leurs objectifs suivants:Comment appliquer le même objectif sur deux enregistrements différents? (? Ou, champ d'enregistrement des noms comme valeurs de première classe)

data Bar = Bar {barField1 :: Int, barField2 :: String} 
makeLensesWith abbreviatedFields ''Bar 

data BarError = BarError {barerrField1 :: [String], barerrField2 :: [String]} 
makeLensesWith abbreviatedFields ''BarError 

Maintenant, tous les deux ont accès aux lentilles field1 & field2 en vertu de la mise en œuvre du HasField1 et HasField2 classes de type. Cependant, je ne peux pas obtenir le morceau de code suivant pour compiler:

-- Most-general type-signature inferred by the compiler, if I remove the 
-- KindSignatures from `record` & `errRecord` below: 
-- 
-- validateLength :: (IsString a) => (Int, Int) -> ALens t t [a] [a] -> t -> t -> t 
-- 
validateLength (mn, mx) l (record :: Bar) (errRecord :: BarErr) = 
    let len = length (record ^# l) 
    in if ((len<mn) || (len>mx)) 
    then errRecord & l #%~ (\x -> ("incorrect length"):x) 
    else errRecord 

-- Usage scenario: 
-- 
-- let x = Bar 10 "hello there" 
--  xErr = BarError [] [] 
-- in validateLength (3, 10) field2 x xErr 

Message d'erreur:

/Users/saurabhnanda/projects/vl-haskell/src/TryLens.hs:18:20: error: 
    • Couldn't match type ‘BarError’ with ‘Bar’ 
     Expected type: BarError -> BarError 
     Actual type: Bar -> BarError 
    • In the second argument of ‘(&)’, namely 
     ‘l #%~ (\ x -> ("incorrect length") : x)’ 
     In the expression: 
     errRecord & l #%~ (\ x -> ("incorrect length") : x) 
     In the expression: 
     if ((len < mn) || (len > mx)) then 
      errRecord & l #%~ (\ x -> ("incorrect length") : x) 
     else 
      errRecord 

Note: Au lieu d'utiliser ^. et %~ J'utilise ^# et #%~ parce que Je voudrais

Modifier: Un extrait plus simple pour illustrer le problème est:

-- intended type signature: 
-- funkyLensAccess :: l -> r1 -> r2 -> (t1, t2) 
-- 
-- type signature inferred by the compiler 
-- funkyLensAccess :: Getting t s t -> s -> s -> (t, t) 
-- 
funkyLensAccess l rec1 rec2 = (rec1 ^. l, rec2 ^. l) 
+0

Type sig please ... – leftaroundabout

+0

Je ne suis pas sûr de la bonne signature de type et je laisse le compilateur déduire la signature de type. Je vais mettre la signature de type inférée dans la question, ce qui rendra évident pourquoi ce n'est pas une vérification de type. Dans ce cas, veuillez considérer l'intention de la question - J'essaie de comprendre comment écrire ce morceau de code. –

+0

@leftaroundabout Fait! –

Répondre

3

Donc, essentiellement, votre problème n'a rien à voir avec les objectifs, mais avec des fonctions (accessor-) qui peuvent fonctionner sur différents types, pour chaque donner un résultat différent typé.

Cela signifie immédiatement un problème: si le type de champ accédé est supposé dépendre du type struct-containing, il s'agit d'un dependent type. Haskell n'est pas un langage typé de manière indépendante. C'est le genre de tâche que vous pouvez facilement faire par exemple. Python en appelant un champ par le nom (sous la forme d'une chaîne), puis opérer sur le terrain via duck typing, mais Haskell efface des informations coûteuses comme chaînes d'étiquettes d'enregistrement à l'exécution pour de très bonnes raisons, et bien sûr le compilateur a besoin connaître tous les types de manière à ce qu'ils ne puissent pas être déduits lors de l'exécution. En ce sens, ce que vous demandez n'est tout simplement pas possible.

Ou est-ce? GHC est devenu assez bon pour les types dépendants. Depuis un certain temps, il est possible de gérer des étiquettes non spécifiques à un type en tant que valeurs de chaîne de niveau de type, appelées Symbol s. Et très récemment, il y a eu work on allowing fields of any record to be accessed by name, c'est-à-dire un peu comme dans Python, mais tous au moment de la compilation, quel que soit le type contenu dans le champ.

L'essentiel est que vous ayez besoin d'exprimer la fonction au niveau du type mappant une étiquette d'enregistrement et un type d'enregistrement à un type d'élément contenu. Ceci est exprimé par le HasField class.

{-# LANGUAGE DataKinds, KindSignatures, FlexibleInstances, FlexibleContexts, FunctionalDependencies, ScopedTypeVariables, UnicodeSyntax, TypeApplications, AllowAmbiguousTypes #-} 

import GHC.Records 
import GHC.TypeLits (Symbol) 

data Bar = Bar {barField1 :: Int, barField2 :: String} 

data BarError = BarError {barerrField1 :: [String], barerrField2 :: [String]} 
deriving (Show) 

type LensOn s a = (a, a -> s) -- poor man's lens focus 

instance HasField "Field2" Bar (LensOn Bar String) where 
    getField (Bar i s) = (s, \s' -> Bar i s') 

instance HasField "Field2" BarError (LensOn BarError [String]) where 
    getField (BarError f₁ f₂) = (f₂, \f₂' -> BarError f₁ f₂') 

validateLength :: ∀ (f :: Symbol) 
         . (HasField f Bar (LensOn Bar String) 
         , HasField f BarError (LensOn BarError [String])) 
    => (Int,Int) -> Bar -> BarError -> BarError 
validateLength (mn,mx) record errRecord 
    = let len = length . fst $ getField @f record 
     in if len < mn || len > mx 
      then case getField @f errRecord of 
       (oldRec, setRec) -> setRec $ "incorrect length" : oldRec 
      else errRecord 

main :: IO() 
main = let x = Bar 10 "hello there" 
      xErr = BarError [] [] 
     in print $ validateLength @"Field2" (3,10) x xErr 

Testé avec GHC-8.3.20170711, doesnt probablement » travailler avec des versions beaucoup plus.

1

Si vous souhaitez qu'une valeur transmise en tant qu'argument fonctionne avec deux types différents, vous devez utiliser l'extension Rank2Types (ou l'équivalent RankNTypes). Puis, puisque les types de rang 2 ou plus ne sont jamais déduits dans GHC, vous devrez écrire la signature de type explicitement.

Notre première passe pourrait ressembler à: IsString a => (Int, Int) -> (forall s a. Lens' s a) -> Bar -> BarError -> BarError Mais, c'est façon trop générale pour ce second argument, si général j'ai tendance à douter existe une valeur non inférieure de ce type. Nous ne pouvons certainement pas y passer field1 ou field2.

Puisque nous voulons transmettre field1 ou field2 nous avons besoin de quelque chose qui unifie leurs types: HasField1 s a => Lens' s a et HasField2 s a => Lens' s a. Malheureusement, puisque HasField1 et HasField2 ne partagent (ou n'ont) aucune super-classe, le seul type qui les unifie le type donné dans le dernier paragraphe. Notez que même si HasField1 et HasField2 partageaient une super classe, nous n'aurions toujours pas terminé. Votre implémentation nécessite également que le champ Bar soit un Foldable et que le champ BarError soit une liste de IsString. Exprimer ces contraintes est possible, mais pas vraiment convivial.