From 24f925ff6df7b3256d60c656d53c9a4b53f526dd Mon Sep 17 00:00:00 2001 From: sgf Date: Wed, 18 Dec 2024 21:47:08 +0300 Subject: [PATCH] new(unmarshal-to-interface): Add variants of unmarshaling json to interface struct fields. --- .../unmarshal-to-interface.go | 157 ++++++++++++++++++ .../unmarshal-to-interface.hs | 121 ++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 unmarshal-to-interface/unmarshal-to-interface.go create mode 100644 unmarshal-to-interface/unmarshal-to-interface.hs diff --git a/unmarshal-to-interface/unmarshal-to-interface.go b/unmarshal-to-interface/unmarshal-to-interface.go new file mode 100644 index 0000000..fe8ea5d --- /dev/null +++ b/unmarshal-to-interface/unmarshal-to-interface.go @@ -0,0 +1,157 @@ + +package main + +import ( + "fmt" + "encoding/json" + "reflect" +) + +// Unmarshal json to a struct containing interface field. See full +// corresponding Haskell code in separate file. + +// Haskell: +// class I a where +// f :: a -> () +type I interface { + f() +} + +type S struct { + X int +} + +var _ I = S{} +func (s S) f() {} + +// Haskell: data R1 = forall a. I a => R1 a Int +// I.e. data constructor type is 'R1 :: forall a. I a => a -> Int -> R1', +// and the type is existential. +type R1 struct { + I I + Y int +} + +// Haskell: data R2 a = R2 a Int +// I.e. data constructor type is 'R2 :: forall a. a -> Int -> R2 a', +// and this is regular (not existential) type. +type R2[T any] struct { + I T + Y int +} + +// Version of 'S' with pointer receiver for use with 'R3'. +type SP struct { + X int +} + +var _ I = (*SP)(nil) +func (s *SP) f() {} + +// Haskell does not have reflect, so there is no equivalent of 'R3'. +type R3 struct { + I I + Y int +} + +func (r *R3) UnmarshalJSON(bs []byte) (err error) { + v := struct { + I json.RawMessage + Y int + }{} + + err = json.Unmarshal(bs, &v) + if err != nil { + return + } + r.Y = v.Y + + if r.I != nil { + p := reflect.ValueOf(r.I) + if p.Kind() != reflect.Pointer { + err = fmt.Errorf("R3.UnmarshalJSON(): Interface 'r.I' uses value receiver '%v'", p.Type()) + return + } + + if err = json.Unmarshal(v.I, p.Interface()); err != nil { + return + } + } + + return +} + +// Haskell: +// class Read a => I2 a where +// f2 :: a -> () +type I2 interface { + f2() + json.Unmarshaler +} + +type S2 struct { + X int +} + +// Dummy instance, but it's required by 'I2'. +var _ json.Unmarshaler = (*S2)(nil) +func (s *S2) UnmarshalJSON(bs []byte) error { + v := struct {X int}{} + if err := json.Unmarshal(bs, &v); err != nil { + return err + } + s.X = v.X + return nil +} + +// Implementing I2 with value receiver has no sense, because +// 'json.Unmarshaler' can only work with pointer receiver. +var _ I2 = (*S2)(nil) +func (s *S2) f2() {} + +// Haskell: data R4 = forall a. I2 a => R4 a Int +// I.e. data constructor is: 'R4 :: forall a. I2 a => a -> Int -> R4' +type R4 struct { + I I2 + Y int +} + +// json.UnmarshalJSON() method in Go has a pointer to actual data, so it +// actually does know what type is behind existential interface 'I2'. But +// Haskell only uses types and does not look at values during type check. +// Therefore, a function equivalent to 'json.UnmarshalJSON()' should have a +// type witness, which tell Haskell, which type to use for interface 'I2'. So, +// the implementation of 'json.Unmarshaler will look like: +// +// class Unmarshaler b a where +// read' :: Proxy a -> String -> b +// +// and an instance for R4 will be: +// +// instance I2 a => Unmarshaler R4 a where + +func main() { + jsBytes := []byte(`{"I": {"X": 1}, "Y": 2}`) + + var err error + + r1 := R1{} + err = json.Unmarshal(jsBytes, &r1) + fmt.Printf("R1: err = %v, r1 = %v\n", err, r1) + + r2 := R2[S]{} + err = json.Unmarshal(jsBytes, &r2) + fmt.Printf("R2: err = %v, r2 = %v\n", err, r2) + + r3 := R3{I: S{}} + err = json.Unmarshal(jsBytes, &r3) + fmt.Printf("R3{S}: err = %v, r3 = %v\n", err, r3) + + r3p := R3{I: &SP{}} + err = json.Unmarshal(jsBytes, &r3p) + fmt.Printf("R3{SP}: err = %v, r3p = %v, r3p.I = %v\n", err, r3p, r3p.I) + + r4 := R4{I: &S2{}} + err = json.Unmarshal(jsBytes, &r4) + fmt.Printf("R4: err = %v, r4 = %v, r4.I = %v\n", err, r4, r4.I) +} diff --git a/unmarshal-to-interface/unmarshal-to-interface.hs b/unmarshal-to-interface/unmarshal-to-interface.hs new file mode 100644 index 0000000..5466099 --- /dev/null +++ b/unmarshal-to-interface/unmarshal-to-interface.hs @@ -0,0 +1,121 @@ +{-# LANGUAGE ViewPatterns #-} + +import Data.List + +-- Here is Haskell equivalent of unmarshaling json to interface in Go. See +-- corresponding Go file for full Go code. + +-- I'll use regular 'Show' and 'Read' instances instead of marshaling to/from +-- json. 'Show' instance will be required during creation of values of 'Rx' +-- types. But it's actually does not matter, where to require it or require or +-- not at all. + +-- Go: +-- type I interface { +-- f() +-- } +class I a where + f :: a -> () + +-- Go: +-- type S struct { +-- X int +-- } +data S = S Int + deriving (Show, Read) + +-- Go: +-- var _ I = S{} +-- func (s S) f() {} +instance I S where + f _ = () + +-- Go: +-- type R1 struct { +-- I I +-- Y int +-- } +data R1 = forall a. (I a, Show a) => R1 a Int +deriving instance Show R1 +-- So data constructor type is: R1 :: forall a. (I a, Show a) => a -> Int -> R1 . +-- I.e. i can't say what 'a' was used by looking at type 'R1' of some 'x :: R1'. +-- And therefore 'Read' instance won't typecheck with: +-- +-- Ambiguous type variable ‘a0’ arising from a use of +-- ‘GHC.Read.readPrec’ prevents the constraint ‘(Read a0)’ from being +-- solved. +-- +--deriving instance Read R1 + +-- Go: +-- type R2[T any] struct { +-- I T +-- Y int +-- } +data R2 a = R2 a Int + deriving (Show, Read) + +-- Go: +-- type I2 interface { +-- f2() +-- json.Unmarshaler +-- } +class Read a => I2 a where + f2 :: a -> () + +data S2 = S2 Int + deriving (Show, Read) + +instance I2 S2 where + f2 _ = () + +-- Go: +-- type R4 struct { +-- I I2 +-- Y int +-- } +data R4 = forall a. (I2 a, Show a) => R4 a Int +deriving instance Show R4 +-- So data constructor type is: R4 :: forall a. (I2 a, Show a) => a -> Int -> R4 , +-- which means, that by looking at value type R4 i can't say what type 'a' to +-- use. Therefore, 'Read' instance can't be written +-- +-- Ambiguous type variable ‘a0’ arising from a use of ‘R4’ +-- prevents the constraint ‘(I2 a0)’ from being solved. +-- +--deriving instance Read R4 + +-- But Go's 'json.Unmarshaler' interface works differently: it lookups +-- instance not just by type 'R4', but it also already has (pointer to) value +-- of type 'R4'. And by looking at value it may tell, what type 'a' was used +-- during its creation. Thus, (because i don't have pointers here) i need a +-- witness of type 'a' to construct value of type 'R4'. +data Proxy a = Proxy + +-- Go: 'json.Unmarshaler' interface. 'Proxy' serves the role of pointer +-- receiver for determining into which interface to unmarshal struct fields, +-- if any. +class Unmarshaler b a where + read' :: Proxy a -> String -> b + +-- Read string produced by 'Show' instance: "R4 (S2 1) 2" +instance (I2 a, Show a) => Unmarshaler R4 a where + read' _ (stripPrefix "R4" -> Just i2str) = + let [(i2, rest)] = readParen True (reads @a) $ i2str + in R4 i2 (read rest) + +-- And here it is (note explicit type applications): +-- +-- ghci> read' @R4 (Proxy @S2) $ show (R4 (S2 1) 2) +-- R4 (S2 1) 2 + +main :: IO () +main = do + let r2 :: R2 S + r2 = read $ show (R2 (S 1) 2) + print r2 + + let r4 :: R4 + r4 = read' (Proxy @S2) $ show (R4 (S2 1) 2) + print r4 + -- 2.20.1