3

I am adding some function to threepenny ui api. I would like to had the ability to draw line with canvas.

The function that I can write have the following signature:

moveTo :: Vector -> UI ()
lineTo :: Vector -> UI ()
stroke :: UI ()
strokeStyle :: UI ()
beginPath :: UI ()

Each primitive moveTo and lineTo should happen in between a beginPath .. stroke call. How would you enforce the beginPath ... stroke sequence. By design I would like to give the user no choice for drawing lines. So the user is not aware of the beginPath ... stroke sequence.

cdk
  • 6,698
  • 24
  • 51
mathk
  • 7,973
  • 6
  • 45
  • 74
  • I suggest going with a functional, semantically meaningful API, like [graphics-drawingcombinators](https://hackage.haskell.org/package/graphics-drawingcombinators), instead of this imperative-style one. The point is, the values you are building are images, not series' of commands. – luqui Jun 27 '14 at 21:10

2 Answers2

2

It's definitely good to design your API so that it can't be used improperly. One approach you might take here is to create an un-exported wrapper that lets you control how these particular actions are composed (I haven't tried to run this, sorry):

-- Don't export constructor
newtype Line a = Line { runLine :: UI a }

-- Wrap the return types in your current implementation with Line, for:
moveTo :: Vector -> Line ()
lineTo :: Vector -> Line ()
...

instance Monad Line where
         (Line ui) >>= f = Line (ui >>= \a-> beginPath >> (runLine $ f a))
         return = Line . return

A couple other points:

  1. You may want to use a Monoid instance instead, if your API doesn't need to bind any values (i.e. all of your line API functions end in -> Line ()

  2. If you need to do something like wrap the entire sequence of composed line actions in e.g. start and end actions or whatever, you could further extend the above with

    runLine (Line ui) = start >> ui >> end
    
jberryman
  • 16,334
  • 5
  • 42
  • 83
1

Here's how I would design a canvas API.

newtype Drawing = ...
instance Monoid Drawing where ... -- for combining drawings

line :: Vector -> Vector -> Drawing
path :: [Vector] -> Drawing
withStyle :: Style -> Drawing -> Drawing
runDrawing :: Drawing -> UI ()

Here the functions operate on semantically meaningful objects (from the user's perspective), rather than imperative commands. This should be implementable with the type

newtype Drawing = Drawing (UI ())

however sometimes subtleties will require that the type have a bit more structure, so be open to that (e.g. Something -> UI ()).

luqui
  • 59,485
  • 12
  • 145
  • 204