-
Notifications
You must be signed in to change notification settings - Fork 562
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement Typeable #4097
base: master
Are you sure you want to change the base?
Implement Typeable #4097
Conversation
I'm reading the conversation in #2243 and, like several of the participants there, I've never had the need for Is that basically right? If so, my current weakly-held opinion is that I'd rather spend compiler code on making working with rows and |
The I think the big difference though is that variants are tagged with their constructor in the encoding, but |
The |
@garyb yeah, problems with subcomponents are precisely what I'm working with myself - for context, I'm currently porting Shpadoinkle (https://shpadoinkle.org) into purescript (https://gitlab.com/thematten/purescript-fluorine). Now, thing is, premise of it's approach is to make composition simple, and while that's easily doable with simple trees of elements, problems arise once you try to add memoization, because while patching, you have no way to know whether new memoized node isn't actually a completely different one, with different type of argument. Halogen solves this with typed approach as mentioned, but such solution degrades user experience a lot, introducing need for additional boilerplate, possibility of new, unproductive type errors and leaks internal details about components being memoized. All of that only to tell compiler which memoized types may appear in some position. And with runtime representation being basically the same, as @rhendric mentioned. This may bite programmers in pretty arbitrary situations, e.g. during refactoring of components. In Haskell, |
I'd love to have parse :: Text -> Either Text Dynamic
parse val
| Just v <- scan val :: Maybe Unit = okay <| Dynamic v
| Just v <- scan val :: Maybe Bool = okay <| Dynamic v
| ...
| otherwise = error <| unwords ["!! Error parsing value", display val |> quote]
insert :: Component a -> Dynamic -> Component a
insert (Field b) (Dynamic b') =
| Just Refl <- b ~= b' -> done <| Field b'
| otherwise -> throw <| CouldNotChangeVal (someTypeOf b) (someTypeOf b')
... I'm not aware of a trick using open variants to achieve the same thing... |
@timjs Yeah, once again you could probably create some top level synonym used in place of Really, |
@TheMatten, that is indeed a possibility, but it assumes a closed world doesn't it? In my example above, the library in which the The application, containing the As a library author, I don't want to make that choice upfront. So closed variants are not an option. Or is there a way an open variant could model that? |
@timjs I imagine you should be able to parameterize context you work in with some variant supplied by the user, similarly to how slots in What I'm trying to say is that while it's hard to come up with usecase which wouldn't be possible to encode with "big enough" |
It does not follow that the maintenance cost is low just because this PR has only 60 lines. The potential maintenance cost of supporting an extra feature which ends up being depended on by users is very difficult to predict, but could be significantly more than you might expect. If this implementation turns out to be incorrect, we'll need to diagnose and fix the bug - and sometimes this significantly complicates the implementation. The initial Coercible PR seemed fine but it needed something like 10 followup PRs to get the feature to a stage where it worked correctly, and by the end I think we had quite a bit more code than we did originally. The feature needs documentation, it generates more questions along the lines of "when should I use feature X rather than feature Y", it could interact poorly with other features, etc. |
@hdgarrood Yeah, I should probably be more concrete about maintenance costs:
|
I am familiar with Variant, but less familiar with Typeable - since here we're talking about implementing a compiler extension, I'm wondering:
|
@f-f To answer your points:
|
I'm 👎 because I don't think this provides enough utility/is in enough demand to justify adding a feature to the compiler, sorry. |
@hdgarrood no problem 🙂 Alternatively, would it be possible to expose needed information e.g. in #4092? |
So adding type information to CoreFn? That is something that comes up from time to time, but I think it's a bigger project than it seems. I don't really know enough about it to be able to comment unfortunately. |
Sorry, I've lost the discussion on To make it concrete. Say I've some container type, which is empty or filled with a value of type {-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
module Typeable where
import Data.Type.Equality
import Type.Reflection
(?=) :: TypeRep a -> TypeRep b -> Maybe (a :~: b)
(?=) = testEquality
-- | A container of type `a`
data Container a
= Empty
| Filled a
-- | Essentialy `Dynamic`
data Input where
Input :: Typeable a => a -> Input
update :: forall a. Typeable a => Container a -> Input -> Container a
update (Empty) (Input y)
| Just Refl <- alpha ?= typeOf y = Filled y
| otherwise = Empty
where
alpha = typeRep :: TypeRep a
update (Filled x) (Input y)
| Just Refl <- typeOf x ?= typeOf y = Filled y
| otherwise = Filled x Above is working Haskell code. I'd do the same thing in Idris by using a sigma type saving a type witness. I can't see how I would use a |
import Prelude
import Data.Maybe (Maybe(..))
import Data.Symbol (class IsSymbol)
import Data.Variant (Variant, prj)
import Prim.Row as Row
import Type.Proxy (Proxy(..))
-- | A container of type `a`
data Container a
= Empty
| Filled a
update :: forall s a r r'. IsSymbol s => Row.Cons s a r r' => Container a -> Variant r' -> Container a
update Empty v = case prj (Proxy :: _ s) v of
Just y -> Filled y
Nothing -> Empty
update (Filled x) v = case prj (Proxy :: _ s) v of
Just y -> Filled y
Nothing -> Filled x |
Thanks a lot @rhendric! Really appreciate it :-) Now I understand the way to use this. Only thing left is an error on unknown type variables in the instance head of IsSymbol in |
Really didn't know In that case I agree with @hdgarrood that |
You will either need to fix |
Yes, exactly. It can be worked around with some type-level fun, as long as everything around it is sufficiently well-typed. Here's a more fully-worked example incorporating your previous module Main where
import Prelude
import Data.Either (Either(..))
import Data.Maybe (Maybe(..))
import Data.Symbol (class IsSymbol)
import Data.Variant (Variant, inj, prj)
import Effect (Effect)
import Prim.Row as Row
import Prim.RowList (RowList, class RowToList)
import Prim.RowList as RowList
import TryPureScript (p, render, text)
import Type.Data.Boolean (False, True)
import Type.Proxy (Proxy(..))
-- Let's pretend we aren't on try.purescript.org
log :: String -> Effect Unit
log = text >>> p >>> render
-- This is something that should be in a library somewhere (maybe it is?)
class FindTypeInRowList :: forall k. Symbol -> k -> RowList k -> Boolean -> Constraint
class FindTypeInRowList s a rl b | a rl -> s b
instance ftirlNil :: FindTypeInRowList s a RowList.Nil False
else instance ftirlNext :: FindTypeInRowList s a (RowList.Cons s a rl) True
else instance ftirlRec :: FindTypeInRowList s a rl' b => FindTypeInRowList s a (RowList.Cons s' a' rl') b
data Container a
= Empty
| Filled a
instance showContainer :: Show a => Show (Container a) where
show Empty = "Empty"
show (Filled a) = "(Filled " <> show a <> ")"
-- This needs a proxy to determine s.
update' :: forall p s a r r'. IsSymbol s => Row.Cons s a r r' => p s -> Container a -> Variant r' -> Container a
update' p Empty v = case prj p v of
Just y -> Filled y
Nothing -> Empty
update' p (Filled x) v = case prj p v of
Just y -> Filled y
Nothing -> Filled x
-- This uses type-level shenanigans to determine s, so doesn't require a
-- proxy as long as the types are sufficiently determined at the call site.
update :: forall s a r r' rl. RowToList r rl => IsSymbol s => Row.Cons s a r' r => FindTypeInRowList s a rl True => Container a -> Variant r -> Container a
update = update' (Proxy :: _ s)
class Scan a where
scan :: String -> Maybe a
instance scanUnit :: Scan Unit where
scan = case _ of
"unit" -> Just unit
_ -> Nothing
instance scanBool :: Scan Boolean where
scan = case _ of
"true" -> Just true
"false" -> Just false
_ -> Nothing
-- purs warns about this _. You could spell out this type entirely as
-- parse :: forall r. String -> Either String (Variant (u :: Unit, b :: Boolean | r))
-- but it'd be nice if you didn't have to.
parse :: String -> Either String (Variant _)
parse val
| Just v <- (scan val :: Maybe Unit) = Right $ inj (Proxy :: _ "u") v
| Just v <- (scan val :: Maybe Boolean) = Right $ inj (Proxy :: _ "b") v
| otherwise = Left $ "error scanning " <> val
main :: Effect Unit
main = do
let v :: Either String (Variant (u :: Unit, b :: Boolean))
v = parse "true"
let c1 :: Container Unit
c1 = Empty
let c2 :: Container Boolean
c2 = Filled false
let result1 = update c1 <$> v
let result2 = update c2 <$> v
log (show result1)
log (show result2) There are some obvious warts here. The biggest is that we need to provide labels One thing I still don't understand is what |
@rhendric sure - I'm trying to implement (BTW, this ignores the fact that the rendering function itself may change. But that's problem for |
I might not understand that example fully because I don't have the larger context of what else is going on in that MR, but let me try anyway. If you get/require an implementation of data DependencyF a = DependencyF (StaticPointer (a -> a -> Boolean)) a
newtype Dependency = Dependency (Exists DependencyF)
instance eqDependency :: Eq Dependency where
eq (Dependency d1) (Dependency d2) =
d1 # runExists \(DependencyF staticEqA a) ->
d2 # runExists \(DependencyF staticEqB b) ->
staticKey staticEqA == staticKey staticEqB && any (\eq' -> eq' a (unsafeCoerce b)) (derefStaticPtr staticEqA)
depending :: forall a m c. StaticPointer (a -> a -> Boolean) -> (a -> Html m c) -> a -> Html m c
depending staticEq f x = Html (\a d b c -> d (Dependency (mkExists (DependencyF staticEq x)))
$ case f x of Html h' -> h' a d b c) |
Hmm, very cool idea, but that wouldn't work with something like |
Yeah, you'd want the production of the But if that general idea is sufficient on the consumer side of things, once you have class StaticEq a where
staticEq :: StaticPointer (a -> a -> Boolean)
instance universalStaticEqInstance :: Eq a => StaticEq a where
staticEq = unsafeThrow "not to be used without some fancy CoreFn transformations"
depending :: forall a m c. StaticEq a => (a -> Html m c) -> a -> Html m c
depending f x = Html (\a d b c -> d (Dependency (mkExists (DependencyF staticEq x)))
$ case f x of Html h' -> h' a d b c) Then rewrite any calls to |
Description of the change
Purescript currently doesn't have
Typeable
constraint in style of Haskell, which makes it basically impossible to implement some functionality, e.g. safe dynamically typed values or memoizing in VDOM where memoized nodes change type of argument.Generic
doesn't help there, because it doesn't see difference between constructors defined in different modules, and external libraries don't help either, because they would require users to implement instances for possibly huge amount of types and would prohibit them from using it with external types (because of orphans being dissallowed).This PR implements minimal version of
Typeable
dictionary generation, expecting API to be defined as follows, e.g. inprelude
:(edit: disallowed instances in style of
Coercible
)where
Typeable
gets resolved automatically in style ofIsSymbol
.TypeRep
contains statically generated hash created from normalized string representation of the type (usingGHC.Fingerprint
).I figured that the change is small enough that instead of just talking about it, I can create prototype that can be either dismissed or refined, so I've created PR instead of an issue. Technically, it fixes #2243, though using much more minimal interface as described above, only providing what's needed to implement functionality like
Dynamic
orin some external library.
As far as implementation goes, things I'm not completely sure about are whether it may interact badly with redundant syntax (seems like kind annotations, parens, operators, synonyms and rows passed to
rowToSortedList
get normalized at that point?) and whetherPolyKinds
may create some unexpected problems (they're hashed too by extracting them fromKindApp
right now).When it comes to compatibility guarantees, I would expect same hashes when building using the same compiler version modulo bugfixes - this means that changes to
GHC.Fingeprint
should probably result in minor version change.Checklist: