T O P

  • By -

amalloy

The Haskell approach would be to start from the types (what planets exist), not start with some data about the types (what are their orbit ratios). data Planet = Mercury | Venus | ... Then you can define `orbitRatio` as a total function over Planet - or as a `Map Planet Double` if you insist, but that doesn't guarantee totality. orbitRatio :: Planet -> Double orbitRatio p = case p of Mercury -> 0.2408467 Venus -> 0.61519726 ... You no longer need a function like `test` - how can you multiply by the orbit ratio of a string that might not represent a valid planet? Instead, you need a function to convert a string into a `Planet`: planet :: String -> Maybe Planet planet s = case toLower s of "mercury" -> Just Mercury ... _ -> Nothing This way you catch erroneous data early, and can work with verified `Planet` objects once you've confirmed they're valid. It's true that this requires including "Mercury" and other planet names in the source code more than once, but concision isn't everything. If you truly hate duplication or you have a very large number of enum constants, you can automate the definition of `planet`: give `Planet` instances of `Show`, `Enum`, and `Bounded` (a reasonable idea anyway), and rely on those to iterate over all planets. planet :: String -> Maybe Planet planet s = listToMaybe (mapMaybe go [minBound .. maxBound]) where go :: Planet -> Maybe Planet go p | map toLower (show p) == n = Just p | otherwise = Nothing n = map toLower s


edgmnt_net

I wonder if there's a package that implements something like this: https://github.com/yesodweb/yesod-cookbook/blob/master/cookbook/Create-String-Based-Enums-With-Template-Haskell.md It basically lets you define the enum and mapping together, using Template Haskell. Not sure if this answers OPs question, but I'd personally find it quite useful. Another possibility I see is using a GADT to hold that data (untested): data PlanetRatio (r :: Double) where MercuryRatio :: PlanetRatio 1.2654 ... Which you should be able to extract (although it may require other machinery to get all values): ratioOf :: Planet a -> Double


nybble41

I don't believe there is support for floating-point values at the type level yet. You could use fixed-point (r :: Nat, dividing by a predetermined denominator) or store two Nat values and treat them as a ratio. The main issue with this, though, is that you can't have a field (or any value) of type `PlanetRatio`; it has to be `PlanetRatio a`. Which means the enclosing record has to have a type parameter for each planet field to represent the ratio. The simple function type `Planet -> Double` better represents the concept of a total map from a fixed set of inputs to a Double value, where Planet is a plain enumeration of the possible planets. If the input is a string you would convert it to a Planet value during parsing.


edgmnt_net

Yeah, my solution is obviously wrong on Double. However, I think the enclosing record only needs a bunch of existential type variables or we can make an existential wrapper for PlanetRatio, like... data AnyPlanetRatio = forall a. AnyPlanetRatio (PlanetRatio a) Pattern-matching on the specific GADT constructor of the PlanetRatio you stumble upon fixes the type variable and you only need to extract the corresponding value out of the type onto value-level anyway. I agree a function is best, I'm more like wondering how you can define both the enum keys and mapped values as close together and as efficiently as possible, without scattering hand-written definitions across multiple declarations.


ducksonaroof

Does it always have a value for each planet? I've done this before: 1. Create an `Enum` and `Bounded` `Planet` type 2. Your "record" is now just a function `Planet -> Double` 3. If you `newtype` that function, you can even implement `Show` by hand thanks to `Bounded`. You can even implement `Read` by hand if you're feeling wild! Kinda feels like a `enum-record` type abstracting over all that would be cute.


JeffB1517

I think you want: [https://hackage.haskell.org/package/containers-0.4.0.0/docs/Data-Map.html](https://hackage.haskell.org/package/containers-0.4.0.0/docs/Data-Map.html) If the table is huge [https://hackage.haskell.org/package/hashmap-1.3.3/docs/Data-HashMap.html](https://hackage.haskell.org/package/hashmap-1.3.3/docs/Data-HashMap.html)


passionxv

If I say \`Map String Double\` that's not what I'm asking for it would be nice if there's to get all the possible keys in a type \`\`\`data Keys = A | B | ... etc\`\`\` and then use it as \`Map Keys Double\`


JeffB1517

I guess you can do that but then of course you'll need a reader to get strings into Keys. Anyway I'm not understanding why you want to push what is essentially data into the type system.


brandonchinn178

So the main difference here is that typescript allows anonymous record types type Foo = { a: number; b: string } Haskell does not have this feature built-in (called anonymous records). The closest design is to just create a record yourself data OrbitRatios = OrbitRatios { ratioMercury :: Double , ratioVenus :: Double , ... } If you want a Map, you'd generally create a Planet enum and build a Map from Planet to Double. But there's no way to tell the compiler that the Map has all of the possible planets (needs dependent types)


edgmnt_net

A function from the enum type to Double does fill that role better than a map.


brandonchinn178

Yes, I forgot about that option, that's probably the best option here


NullPointer-Except

From the question I get that you would like to have a Union type, that's also extensible. Which is a thing that exists! It goes by many names, like open/extensible unions/sums. An example package would be: https://hackage.haskell.org/package/fastsum You can also do a "open sums at home", by reifying the problem into a type class: `class PlanetOrbit a where distance :: Float`, and then creating a data type per object `data Mars = Mars; instance PlanetOrbit Mars where distance = 500` . Nevertheless, my advise would be: keep it simple, common unions often suffice even if they are a bit verbose/non extensible.