File: one-liner-surgery.hs

package info (click to toggle)
haskell-generic-data 1.1.0.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 304 kB
  • sloc: haskell: 2,577; makefile: 6
file content (148 lines) | stat: -rw-r--r-- 3,783 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
{-# LANGUAGE
    DataKinds,
    DeriveGeneric,
    FlexibleInstances,
    LambdaCase,
    MultiParamTypeClasses,
    ScopedTypeVariables,
    TypeApplications,
    TypeFamilies,
    TypeOperators #-}

{-# OPTIONS_GHC -Wno-name-shadowing #-}

-- Example using one-liner and generic-lens
-- on a synthetic type obtained by surgery.

import Control.Applicative ((<|>))
import Data.Coerce (coerce)
import Data.Functor.Identity (Identity(..))
import Data.Kind (Type)
import GHC.Generics (Generic)
import Text.Read (readMaybe)
import Test.Tasty
import Test.Tasty.HUnit

import Data.Generics.Product (field_)  -- generic-lens
import Generics.OneLiner (nullaryOp, binaryOp)  -- one-liner
import Generics.OneLiner.Binary (gtraverse)

import Generic.Data.Microsurgery (DOnFields)

-- | Toy configuration record type.
data Config = C {
    a :: Int,
    b :: Int,
    c :: String
  } deriving (Eq, Generic, Show)

-- | Applying the 'DOnFields' surgery to get a type isomorphic to:
--
-- > data Config = C {
-- >     a :: Maybe Int,
-- >     b :: Maybe Int,
-- >     c :: Maybe String
-- >   }
--
-- See also "Functor functors" and "Higher-kinded data" for a more general pattern:
--
-- - https://www.benjamin.pizza/posts/2017-12-15-functor-functors.html
-- - https://reasonablypolymorphic.com/blog/higher-kinded-data/
--
type PartialConfig = DOnFields Maybe Config

-- | Example
file1 :: [String]
file1 = [
    "a=11",
    "b=33"
  ]

-- | Example
file2 :: [String]
file2 = [
    "b=2",
    "c=Hello"
  ]

-- | Helper for 'emptyOM' and 'mergeOM' below.
class    (a ~ Maybe (UnMaybe a)) => IsMaybe a
instance (a ~ Maybe (UnMaybe a)) => IsMaybe a

-- | Helper for 'IsMaybe' above.
type family UnMaybe (a :: Type) :: Type where
  UnMaybe (Maybe b) = b

-- |
-- > emptyOM = C {
-- >     a = Nothing,
-- >     b = Nothing,
-- >     c = Nothing
-- >   }
emptyOM :: PartialConfig
emptyOM = nullaryOp @IsMaybe Nothing

-- | Helper for 'parseOM' (actually a function from lens).
--
-- @(l .~ b) s@: set the field of record @s@ focused by lens @l@ to @b@.
--
-- > let f = (field_ @"a" .~ v) in
-- > f (C {a = x, b = y, c = z})
-- >
-- > -- equals --
-- >
-- > C {a = v, b = y, c = z}
--
(.~) :: forall s t a b. ((a -> Identity b) -> s -> Identity t) -> b -> s -> t
(.~) l b = coerce l (const b :: a -> b)

-- | Parse lines of a config file.
parseOM :: [String] -> PartialConfig
parseOM = foldr ($) emptyOM . map (\case
  'a' : '=' : n -> field_ @"a" .~ readMaybe n
  'b' : '=' : n -> field_ @"b" .~ readMaybe n
  'c' : '=' : s -> field_ @"c" .~ Just s
  _ -> id)

-- | Merge two records of 'Maybe' fields, keeping the leftmost 'Just' for each
-- field.
mergeOM :: PartialConfig -> PartialConfig -> PartialConfig
mergeOM = binaryOp @IsMaybe (<|>)

-- | Example
parsedOpts12 :: PartialConfig
parsedOpts12 = parseOM file1 `mergeOM` parseOM file2

-- | Helper for 'validateOM' below.
class    (a ~ Maybe b) => FstIsMaybe a b
instance (a ~ Maybe b) => FstIsMaybe a b

-- | Check that all fields are populated with 'Just' and create a plain
-- 'Config' record. If any field is 'Nothing', returns 'Nothing'.
validateOM :: PartialConfig -> Maybe Config
validateOM = gtraverse @FstIsMaybe id

-- | Example
opts12 :: Maybe Config
opts12 = validateOM parsedOpts12

main :: IO ()
main = defaultMain test

test :: TestTree
test = testGroup "one-liner-surgery"
  [ testCase "opts1" $
      "C {a = Just 11, b = Just 33, c = Nothing}" @=? show (parseOM file1)

  , testCase "opts2" $
      "C {a = Nothing, b = Just 2, c = Just \"Hello\"}" @=? show (parseOM file2)

  , testCase "opts12" $
      Just C {a = 11, b = 33, c = "Hello"} @=? opts12

  , testCase "opts1-incomplete" $
      Nothing @=? validateOM (parseOM file1)

  , testCase "empty" $
      "C {a = Nothing, b = Nothing, c = Nothing}" @=? show emptyOM
  ]