14 KiB
Jack Attack
-
Your comments are very thorough! Have you, however, consider the special "haddock" syntax for your documentation? For instance, in your
Object
record, you clearly describe what each field does. If you used the "haddock" syntax (maybe something like--^ comment goes here
), you'd be able to generate static documentation pages much like those you see on hackage or stackage. Those are useful because they allow the user to browse the documentation for your package from the web before deciding to install and use it. And, of course, I think it's nicer to look at a web page than comments! -
There's a lot of duplicate functionality in your
doTimedTransform
function compared to yourdoTransform
function. It seems almost as though there should be one function that takes care of both the interpolation and the transformation; perhapsdoTransform
can take an argument0.0..1.0
of "progress", and go from there. This will save you the trouble of unpacking and repacking yourTransformation
objects. Even if you don't go for that approach, however, there's still an improvement you can make. Rather than making yourdoTimedTransform
function do everything, you may want to define a functiontween :: Object -> Float -> Transformation -> Transformation
This function would do kind of like what your
doTimeTransform
does: it would interpolate the transformation given the current object and the "progress" within the transformation. The difference, though, is that it will not actually apply the transformation, but create a new one (much like you're already doing indoTimedTransform
anyway). You would then be able to feed this transformation intodoTransform
, and it would work as expected. The advantage here is that you won't have to be writing duplicate code for stuff like:doTransform (Combine xs) $ doTransform x obj doTimedTransform (seconds, x) (doTimedTransform (seconds, Combine xs) obj elapsed) elapsed
There's a fairly clear similarity (and consequently duplication) here, and that's what I think my approach would suggest.
And there's one more thing: does
tween
really need access to the object being animated? It sure seems to if your transformations are "absolute", like "move to (1,2)". This "absoluteness" of transformations forces you to useobj
indoTimedTransform
, and would also force you to use it intween
. But if your transformations were delta based ("move left 1, up t2"), yourtween
could be only a function of the current "progress"Float
:tween :: Float -> Transformation -> Transformation
This may help clean up some of the code, and it also allows for an interesting experiment: what if you compose
tween
with various functionsFloat -> Float
? For instance, you could compose it with\x -> x * x
, which I think would make your animation start out slow and speed up at the end. You may be able to use this to play around with various ways of animating your objects that are used in "actual" animation: from what I know, linear transformations are not as common as, say, sigmoid ones (animation starts slow, gets fast in the middle, and slows down again at the end). -
I would try to factor out the "seconds" part of your calculations as soon as possible. In my opinion, it would be clearer to pass around a "percentage completed" float, especially if you don't have any other dependence on the current time.
-
Cody is right, you do have a couple of places that can be used by folds or other higher order functions. For instance,
getAniLength
is justsum . map fst
, and theCombine
case ofdoTransform
is something likefoldr (flip doTransform) obj (x:xs)
. -
Are transformations a monoid? I suspect that they are, with
mempty = Wait
,mappend x y = Combine [x,y]
, andmconcat = Combine
. I don't know if this will be useful, but it's always nice to have access to more standard library functions! -
I'm not sure I follow the
AST
vsLib
modules. They both seem to have something in mind, but I can't quite figure out how they work together. Does theAST
eventually produce aTransformation
? I just don't really know what to make of it!
About your design questions:
- Q1: It would be cool if you had a slider that lets you play the animation back and forward (or at an arbitrary point in time). I think it may help in debugging:
Why did my object just make a 360 and walk away?
It would be even better if you could label your transformations with the function / piece of code that induced them, so that at any point, you can kind of see what part of your code is causing the current transformation.
-
Q2: At the implementation level, you may need to replace the
Float
argument ofdoTransform
with anEnv
argument, which may be a record-like thing that contains the current mouse position, the current time, and so on. You'd then thread it as before.At a language level, things are more interesting. Have you looked into functional reactive programming? You may be interested in encoding transformations declaratively, given, say the current mouse position, time, etc. as input. Your programs would then be descriptions of how to convert these inputs into figures on the screen.
Elm is a language that implements quasi-FRP. It doesn't deal with continuous inputs like you would be, but it encodes programs as transition functions (
Event -> State -> State
). You may be interested in adapting a similar approach - it's a nice way to deal with "imperative" environments from a functional language. -
Q3: This is a tough one! I think you can come up with a textual functional language that can be used for your domain, but I'd be thrilled to see how you may incorporate visual (scratch-like) elements into your project. At this point, though, I can't help but wonder: would you have time to implement something like Scratch-like blocks? We're in week 8, after all. That would be a very graphical type of user interface, and I can't personally imagine defining a UI from scratch, in Haskell, using only graphics library primitives. Of course, you're probably more versed than I am in Haskell's graphics tooling, but it seems to me like a fairly daunting task.
Escape
-
I played with your game for a bit, pretty cool! I think you did a nice job with the textual users interface; it feels pretty much like I'd expect a command line interface to work. I did run into an error on my first playthrough, and decided not to try again:
Prelude.head: empty list
. This happened because I typedsearch
without any arguments. Now, I understand that what I did was an incorrect command, but crashing the game doesn't seem like the right thing to do!head
is a dangerous function; I think that if Haskell were redesigned today, from the ground up, this function would no longer have type[a] -> a
. In general, exceptions in Haskell are a bit of an antipattern, because they're not indicated by the types (how could you knowhead
throws an exception), but also because they're cumbersome to work with!Languages defined after Haskell actually define
head
with aMaybe
. Here's the Idris version, and here's the Elm version. You may want to define your own head function,safeHead :: [a] -> Maybe a
! You can then use pattern matching or themaybe
function to properly handle the error that occurs when the user doesn't give the command enough arguments. You could, for instance, print "Search what?" when a user types in "search". -
This is a minor nitpick, but I'm pretty sure
snake_case
is not the preferred standard for Haskell variable names. Your records (and module names!) should be incamelCase
. In fact, my linter plugin covers the screen with warnings about style when it sees your code! -
Funcfunctor
is a funny name for a typeclass. It seems to imply thatt
is a functor; however,Functor
is a type constructor class, whereas your code treatst
as just a type, not a type constructor! In fact, I'm really not sure what to make of the wordFuncfunctor
. What does it mean? -
On the other hand, I think that this is a good application of type classes. If you want to print a variety of different objects on the screen in your command line game, defining a common type class with the various properties of these objects is a great idea. You can certainly get a very consistent interface this way!
-
In
Escape_funs
, a lot of your pattern matching code can be significantly simplified! You always write out stuff likeObject {object_id=id, object_name=...}
, but use only one of these variables (for instance,id
). Instead, you can only writeObject {object_id=id}
-- that's it! You don't need to manually enumerate every single key in your records to pattern match on them.A cute fun fact of this notation: it's "preferred style" according to Haskell's linter to rewrite code like
(Constructor _ _ _ _)
(pattern matching on a data type and ignoring all its fields) asConstructor{}
. -
foldr (||) False (map (==(get_key obj)) list)
is good use of a fold a map, but it also corresponds to the Haskell functionany
:any (==(get_key obj)) list
. -
Notice how your
run_code
is a bit fragile: yourhelp
function prints out all the commands available to the user, whilerun_code
actually implements these commands. It must be very easy to editrun_code
to add a new command, while forgetting to updatehelp
. You may be able to work around this by defining aCommand
data structure: maybe it'll have adescription
field that you print to the user, and anotheraction
filed that's maybe some monad likeIO ()
that actually performs the actions that your command does. Then, you can have a global list of commandscommands :: Map String Command
, which contains all the allowed commands in your game. Yourhelp
function would go through thiscommands
variable and print out thedescription
field of every available command; on the other hand, therun_code
function would read a command from the user, look it up incommands
, and try to execute theaction
field. -
Notice that you're explicitly handling your application's state: you will need to pass the correct house to
run_code
every time, which means that for each command, you need a separate call torun_code
with whatever house that command puts your player in.An alternative approach is to handle state implicitly. You won't need to take a
house
argument explicitly, and you therefore won't have to explicitly pass it to your recursive calls torun_code
. In your case, this would be best done with aState
monad, since the current location of the player is, well, a piece of state.The question, though, becomes: how the heck do you incorporate a state monad into your
IO
-based code? The answer to that is theStateT
monad transformer. We haven't learned about these in class just yet, but we will soon, and I think it would serve you well! Instead of making your code of typeIO ()
(what it currently is), you'd use something likeStateT [Room] IO ()
. Your calls toputStrLn
would have to go through aliftIO
, but you'd also gain access to functionsget
andput
, which are the functions of the State monad that we have covered in class. Then, whenever your user goes to a different house, you'd runput new_house
, and whenever you need to check the current location of the player, you'd runget
.You may then incorporate other monad transformers into this implementation; I think of interest to you would be
ExceptT
(which is not the same as Haskell's exceptions that I mentioned in my first bullet point!), which might help you exit out of your code when your player types "exit". -
By the way, you should probably explicitly type out the signatures of your function; I think that's considered good style. For instance, for
run_code
, you'd write:run_code :: Player -> [Room] -> IO () run_code plauer house = ...
Your questions:
-
Q1: Hey, I brought up
StateT
andExceptT
and monad transformers in general earlier! Check them out. I think you may find them useful for what you're doing. The only difficulty is that you really need to see a few examples of code before-and-after the use of State and Exception monads to be comfortable in applying them (at least for me! It took me a while to properly understand monads, and more time to understand monad transformers). -
Q2: Haskell's
IO
monad hasreadFile
andwriteFile
methods, but these may be a bit basic for your taste. You may be interested in various formats like INI or even JSON (for which you can use something like Aeson, especially itsencodeFile
anddecodeFile
methods).
I'm running out of steam writing this comment, but I may post a follow up if I think of anything else!
Eric, if you're reading this: have I been evangelizing monad transformers too much? I feel like I have been. It would be good to know if I'm being overzealous; everything starts to look like a nail when all you have is a hammer!