agrafix / Spock

Another Haskell web framework for rapid development
https://www.spock.li
678 stars 56 forks source link

More domain-specific contexts? #46

Closed lunaris closed 9 years ago

lunaris commented 9 years ago

Hi Spock team,

I'm really enjoying the context features that have been added in 0.9.*, but am finding the use of Data.HVect as the implementation (at least in its raw state) a bit jarring. I appreciate the desire for code re-use, but was wondering if you would consider implementing/accepting PRs to improve the situation or simply implement these features within Spock itself? Two possible approaches I see are:

--  Spock defines:
class InContext cxt a where
  fromContext :: MonadIO m => ActionCtxT cxt m a                                    

instance (cxt ~ HVect cts, ListContains n a cts)                   
      =>  InContext cxt a where                                                   

  fromContext                                                         
    = findFirst <$> getContext   

--  Consumers can now write:
f :: InContext cxt Foo => ActionCtxT cxt m a
f
  = do
      foo <- fromContext
      g foo
      ...

g :: Foo -> ActionCtxT cxt m a
...
proxy :: Proxy k                                                                   
proxy = undefined                                                                      

data Context (as :: [*]) where                                                     
  EmptyContext :: Context '[]                                                      
  StackedContext :: a -> Context as -> Context (a ': as)                           

type family IndexOf (a :: k) (as :: [k]) :: Nat where                              
  IndexOf a (a ': as) = 'Zero                                                      
  IndexOf a (b ': as) = 'Succ (IndexOf a as)                                       

class InContextN (n :: Nat) cxt a where                                         
  fromContextN :: MonadIO m => proxy n -> ActionCtxT cxt m a                    

instance (cxt ~ (a ': as)) => InContextN 'Zero cxt a where                      
  --  Base case of what is currently getContext
  fromContextN _                                                                
    = undefined              

instance (InContextN n cxt a, scxt ~ (b ': cxt))                                
      =>  InContextN ('Succ n) scxt a where                                     

  --  Recursive case of what is currently getContext       
  fromContextN _                                                                
    = undefined                                     

type InContext cxt a                                                            
  = InContextN (IndexOf a cxt) cxt a

fromContext :: forall a cxt m.(InContext cxt a, MonadIO m)                      
            => ActionCtxT cxt m a                                               

fromContext                                                                     
  = fromContextN (proxy :: Proxy (IndexOf a cxt)) 

These ideas might both be terrible and are of cause based on just my opinions, but I'd be interested to hear the team's thoughts. For the record, I'm using the first solution in my current project and it's working fairly nicely.

agrafix commented 9 years ago

The initial idea of contexts was to make it as flexible as possible so that the user can or can not use it with type level features like HVect or even pick another dependently typed data structure. I agree that HVect is not the optimal interface yet, but I went with it because it's a dependency of Spock anyway (due to the typesafe routing implementation in reroute) and it worked pretty good to get contexts out in the wild. I am certainly looking for ways to improve it so your issue is very welcome!

Comments on your ideas 1 and 2:

  1. The idea is very small and easy to implement - I find myself writing that fromContext function quite often in different projects. The cool thing is, that this will sort of abstract away the implementation of the context and you can swap it out any time. I think it will need two or three more functions aka removeFromContext, addToContext, initialContext to provide a full abstraction. I have not played with this yet, but I am not sure if removing the position of the value in the context n type variable from the type signature might cause trouble for type inference or even proving that something in the body should also have this value at that position in the context?
  2. This idea looks very clean! I like it! Nevertheless it currently has the same flaw as HVect: You could insert the same type twice into the context and you'll never be able to retrieve the "one in the back". I'd really like to solve this problem too - I currently have two ideas but I have not explored them yet: a) Switch to typed sets implying that every type can only be inserted once or otherwise the value at that position will be overwritten b) Switch to dependent maps see dependent-map c) Switch to row polymorphism (maybe with a library like vinyl) Personally I'd really like to go with approach c because it will maximize composability of contexts but I'm not sure how friendly and practical the interface can be made to such an approach.

Let's discuss! :-)

lunaris commented 9 years ago

Indeed, I've completely missed the point -- your implementation does nothing to force me to use HVects at all (!), making many (if not all) of my points moot. Of course, it might be cool to provide various Web.Spock.Contexts.* modules (e.g. .Dependent, .Lens, .Simple, etc.) but arguably these are outside the scope of the core library and may belong elsewhere. For example, I am finding my implementation of suggestion 1 in the posts above to be quite effective (in essence a simple abstraction over HVect ala the InContext class and some functions in the vein of your addContext etc.), but others might prefer a single-type context and the use of lens' (or similar) Has type class machinery to extract fields from that known type.

Thoughts? Sorry for yet more untargetted rambling.

agrafix commented 9 years ago

Yes, a reasonable plan for this could be to introduce a Spock-context-XXX package family implementing various ideas. That way we can play with ideas and see what works and what doesn't.

agrafix commented 9 years ago

I'll close this issue for now as it is not directly a Spock issue. If you'd like to discuss more, feel free to shot me an email.