Thu Jan 22, 2015

That... felt like a fucking year.

I'm well aware that I've been away from the blog for about a month at this point, but it felt like a fuckton longer. Not that I haven't been programming, mind you. I've been relatively busy, as you can see by my github history. I guess that's what happens when programming is one of your stress-release activities.

Anyhow; now that my mind is a-twirl, my new spawn is home again, and my nose has shed the traditional chitinous exoskeleton of the winter season, lets take a look at what I've been thinking about.

Putting my Parens where my Mouth Is

cl-notebook got a bit more work thrown at it. One or two minor issues fixed, and we've got a working macroexpander now. That's basically it in terms of work on it. As for work with it, I've started writing an HTML5 edition of Lemonade Stand, and I'm thinking about a few other web games/creativity toys. Its shaken out a few obvious features that I'd want from a proper IDE, and a few obvious bugs that I need to squish. Including, but not limited to:

I'll post code eventually, but I want to fix up a few things in cl-notebook first.

And since we're on the subject of cl-notebook, why is this still a pre-beta, even though I've gotten a working macroexpander together? Glad you asked.

One of the other things I've been doing, which will eventually get its own write-up as soon as I get it running well enough to call a decent proof-of-concept, is thinking about history in general and concurrent history in particular. One of the things that came about as a result of those experiments is a more thorough development of the idea of history-aware data stores. Or rather, the first part of said development. And along with the fact that our existing storage approach for notebooks is causing some issues, this has me reconsidering the on-disk file format. A related issue is the on-disk size of chart cells. Recent experiments with delta storage have me thinking of ways to mitigate that.

Audio Sequencer

Back to creativity toys, this is the single most annoying one I'm likely to actively try to develop.

It's a little program that I'm intending for the use case of getting my first son comfortable with tablets, and with the idea of controlling machines around him. That's basically as focused as the use case gets, and this is likely not the last program I'm writing with the same goals. I'm not going to bother going through it because it was written in a fit of complete stupidity, and doesn't at all reflect the final architecture I wanted for the project. The reason I wanted to talk about it is that I wanted to get it working through haste.

To that end, I got haste checked out and made these edits:

Changes from refs/remotes/origin/master to working tree
4 files changed, 119 insertions(+)
 haste-compiler.cabal                   |  2 +
 lib/Audio.js                           | 37 ++++++++++++++++
 libraries/haste-lib/haste-lib.cabal    |  1 +
 libraries/haste-lib/src/Haste/Audio.hs | 79 ++++++++++++++++++++++++++++++++++

        Modified   haste-compiler.cabal
diff --git a/haste-compiler.cabal b/haste-compiler.cabal
index a034774..a944cd5 100644
--- a/haste-compiler.cabal
+++ b/haste-compiler.cabal
@@ -35,6 +35,7 @@ Data-Files:
+    Audio.js

 Flag portable
@@ -258,6 +259,7 @@ Library
+        Haste.Audio
        New        lib/Audio.js
diff --git a/lib/Audio.js b/lib/Audio.js
new file mode 100644
index 0000000..0ea1e31
--- /dev/null
+++ b/lib/Audio.js
@@ -0,0 +1,37 @@
+function jsAudioNew (fnames) {
+    var a = new Audio();
+    var ins = {};
+    fnames.forEach(function (fname) {
+       var low = fname.toLowerCase();
+       if (low.endsWith("ogg")) {
+           ins.ogg = fname;
+       } else if (low.endsWith("wav")) {
+           ins.wav = fname;
+       } else if (low.endsWith("mp3")) {
+           ins.wav = fname;
+       }
+    });
+    if (a.canPlayType('audio/mpeg;') && ins.mp3) {
+       return new Audio(ins.mp3);
+    } else if (a.canPlayType('audio/ogg; codecs="vorbis"') && ins.ogg) {
+       return new Audio(ins.ogg);
+    } else if (a.canPlayType('audio/wav; codecs="1"') && ins.wav) {
+       return new Audio(ins.wav);
+    } else {
+       console.log("No file type supported...");
+    }
+function jsAudioPlay (aud) { aud.play() }
+function jsAudioPause (aud) { aud.pause(); return aud.currentTime }
+function jsAudioGetDuration (aud) { return aud.duration }
+function jsAudioGetCurrentTime (aud) { return aud.currentTime }
+function jsAudioSetCurrentTime (aud, off) { aud.currentTime = off }
+function jsAudioGetVolume (aud) { return aud.volume }
+function jsAudioSetVolume (aud, vol) { aud.volume = vol }
+function jsAudioGetLoop (aud) { return aud.loop }
+function jsAudioSetLoop (aud, bool) { aud.loop = bool }
        Modified   libraries/haste-lib/haste-lib.cabal
diff --git a/libraries/haste-lib/haste-lib.cabal b/libraries/haste-lib/haste-lib.cabal
index 76d7a58..5b76f80 100644
--- a/libraries/haste-lib/haste-lib.cabal
+++ b/libraries/haste-lib/haste-lib.cabal
@@ -33,6 +33,7 @@ Library
+        Haste.Audio
        New        libraries/haste-lib/src/Haste/Audio.hs
diff --git a/libraries/haste-lib/src/Haste/Audio.hs b/libraries/haste-lib/src/Haste/Audio.hs
new file mode 100644
index 0000000..2d10064
--- /dev/null
+++ b/libraries/haste-lib/src/Haste/Audio.hs
@@ -0,0 +1,79 @@
+{-# LANGUAGE CPP                      #-}
+{-# LANGUAGE ForeignFunctionInterface #-}
+module Haste.Audio ( Audio(..), Status, audio
+                   , play, pause, stop, jump
+                   , mute, unmute, setVolume ) where
+import Haste.Prim
+#ifdef __HASTE__
+foreign import ccall jsAudioNew :: [JSString] -> IO JSAny
+foreign import ccall jsAudioPlay :: JSAny -> IO ()
+foreign import ccall jsAudioPause :: JSAny -> IO Float
+foreign import ccall jsAudioGetDuration :: JSAny -> IO Float
+foreign import ccall jsAudioGetCurrentTime :: JSAny -> IO Float
+foreign import ccall jsAudioSetCurrentTime :: JSAny -> Float -> IO ()
+foreign import ccall jsAudioGetVolume :: JSAny -> IO Float
+foreign import ccall jsAudioSetVolume :: JSAny -> Float -> IO ()
+foreign import ccall jsAudioGetLoop :: JSAny -> IO Bool
+foreign import ccall jsAudioSetLoop :: JSAny -> Bool -> IO ()
+jsAudioNew = error "Tried to use jsAudioNew on server side!"
+jsAudioPlay = error "Tried to use jsAudioPlay on server side!"
+jsAudioPause = error "Tried to use jsAudioPause on server side!"
+jsAudioGetDuration = error "Tried to use jsAudioGetDuration on server side!"
+jsAudioGetCurrentTime = error "Tried to use jsAudioGetCurrentTime on server side!"
+jsAudioSetCurrentTime = error "Tried to use jsAudioSetCurrentTime on server side!"
+jsAudioGetVolume = error "Tried to use jsAudioGetVolume on server side!"
+jsAudioSetVolume = error "Tried to use jsAudioSetVolume on server side!"
+jsAudioGetLoop = error "Tried to use jsAudioGetLoop on server side!"
+jsAudioSetLoop = error "Tried to use jsAudioSetLoop on server side!"
+data Audio = Audio { src :: [JSString], file :: JSAny
+                   , loop :: Bool, volume :: Float, duration :: Float
+                   , status :: Status, muted :: Bool } deriving (Eq, Ord, Show)
+data Status = Stopped
+            | Paused Float
+            | Playing deriving (Eq, Ord, Show)
+audio :: [JSString] -> IO Audio
+audio fnames = do f <- jsAudioNew fnames
+                  dur <- jsAudioGetDuration f
+                  return $ Audio { src=fnames, file=f, duration=dur
+                                 , loop=False, muted=False, status=Stopped
+                                 , volume=1.0 }
+play :: Audio -> IO Audio
+play a = do _ <- jsAudioPlay $ file a
+            return $ a { status=Playing }
+pause :: Audio -> IO Audio
+pause a = do off <- jsAudioPause $ file a
+             return $ a { status = Paused off }
+stop :: Audio -> IO Audio
+stop a = do _ <- jsAudioPause $ file a
+            _ <- jsAudioSetCurrentTime (file a) 0.0
+            return $ a { status = Stopped }
+jump :: Audio -> Float -> IO Audio
+jump a off = do _ <- jsAudioSetCurrentTime (file a) off
+                return $ a
+mute :: Audio -> IO Audio
+mute a = if muted a
+         then return a
+         else do _ <- jsAudioSetVolume (file a) 0.0
+                 return $ a { muted=True }
+unmute :: Audio -> IO Audio
+unmute a = if muted a
+           then do _ <- jsAudioSetVolume (file a) $ volume a
+                   return $ a { muted=False }
+           else return a
+setVolume :: Audio -> Float -> IO Audio
+setVolume a vol = do _ <- jsAudioSetVolume (file a) $ vol
+                     return $ a { muted=False, volume=vol }

Which seemed to work, as far as GHC is concerned. No compiler-time errors, and it looks like the JS side of things is doing exactly what I wanted it to, but I'm not submitting a pull request until I can actually compile the thing with hastec and get it working in an actual browser. Which I can't do, because the local version of haste I checked out refuses to haste-boot. And that wouldn't be too big a deal, except trying to revert to the latest haste from cabal doesn't seem to work either, giving me various cabal package dire warnings and errors.

It seems that the last time I got haste running, I did so in a brief shining moment between major library changes while that was actually possible rather than theoretically possible.

Installation Frustration

So haste won't compile, either from source or from cabal. According to the comments I've gotten on this question, I should use cabal sandboxes. Sounds fine in theory. In practice, that page I just linked you to starts with the section title "Building Cabal from git". Fine I guess. That version of cabal|2| installs libraries that demand a newer version of network. Including some dependencies of haste. That newer version of network has split apart network-uri, which apparently means that libraries expecting the old interface are SOL for a while|3|.

Of course, I'm giving you the after-the-fact summary of this. In reality, I haven't been this frustrated with a programming language since I had to install Hunchentoot by hand. Or since the last time I kicked at rebar. Frankly, this episode has me backing away from recommending Haskell use anywhere I'd need to maintain. And seriously considering a separate virtual machine for Haskell development|4|. I'm probably more skittish than most because my window manager also happens to be Haskell-based, which means that borking cabal has immediate, annoying consequences for me beyond the inability to compile .hs files. It still seems rational to keep my Haskell dev-environment separated by a machine boundary with snapshots if a change in one library can cause this kind of ridiculousness.

After messing around with that for a little while, I did a quick search for SML-based alternatives. Haskell fills three voids in my language line-up. It's at once the non-strict, purely functional and strongly-typed piece of my conceptual arsenal. Granted, SML only hits one third of those, but the other two are approachable even in languages that don't support them directly. Turned out to be a moot point though. The SMLserver site was down at the time, which is less than encouraging. Not giving me HTTP errors or anything, just hanging at the Connecting... stage. Also, the SML on Stilts hello-world app is surprisingly wordy.

So I think that's it for now. I'm going to take a serious break from both languages|5| for the next little while. Hopefully, by the time I get around to doing a fresh install of Jessie Beta 2, the respective language libraries have their shit relatively sorted and I can get back to just writing code that can actually work anywhere.

Environments and Methods

Last thing for this edition, I swear. Some small pieces of work have been done on experimentaLISP, but I've mostly been thinking about it. Which is mildly disappointing from my perspective, given that this is supposed to be my experimental staging grounds. The particular thing I'm thinking about is methods. Which is actually to say, "how do you make operations generic over several types in something approaching a sane way?" The obvious way of doing it is pretty straight-forward:

That more or less works|6|, but comes with the price that our definition operation really has to be defgeneric!. It mutates some piece of global state to achieve its goals, and it takes away the ability to locally override particular methods.

A more elaborate approach would be making generics non-top-level lookup tables, which would allow us to override individual method definitions on an environment-frame level, but it suddenly causes method lookup time to go from log n to m log n, where n is the number of methods in this particular generic function, and m is how many override tables exist between the caller and the top level. This is because instead of simply looking up the given name in our environment, then looking up the appropriate method in the result and erroring if we fail to find one, we need a more elaborate handling of that last case. If we don't find the method we need in a particular table, it might be in the next one up, so we need to repeat lookup with this generics' parent environment right up to the global environment. I could pull my usual and trade space for time by copying out the previous level of generic table before inserting overriding methods, which might just be a satisfying enough conclusion. I get the feeling that we'd be able to thin them all down substantially during any hypothetical compilation phase.

A third approach, which removes method tables entirely, is to just store the acceptable type of a given function along with its definition, and allow multiple functions with the same name, but different type signatures. Looking things up again becomes expensive, since it's no longer enough to find a matching name; we instead need to find a matching name attached to a signature that unifies with the current arguments. This also implies evaluating arguments before procedures during application|7|.

No idea which of these approaches, if any, I'll actually end up taking. I just wanted to get them far enough outside of my own head to see them properly.


1 - |back| - Though, oddly, not in a raw SBCL prompt, where an interrupt is signalled as an sb-sys:interactive-interrupt.

2 - |back| - Or maybe it's not cabal, but some completely irrelevant hackage change.

3 - |back| - And the proffered workaround may or may not work, but it gives me enough "you need to re-install library-x which may break library-y" warnings that I don't really want to chance it.

4 - |back| - Incidentally, it's at more or less this point that I found out VirtualBox doesn't really work on Jessie. Something to do with the kernel version, and some set of config calls that don't happen properly anymore, resulting in an improper init.d. Digging didn't really do anything; other people have had this problem, but they solved it by doing things I've already tried to no effect. After thinking about it for a few minutes, I decided that I probably didn't want VirtualBox for this use-case in the first place. What I'd ultimately want is a Debian or similar guest running on top of a Debian host. This is something that Xen or Qemu could do fine. In the end, the presence of pre-built Wheezy images for Qemu made me take that route. Still, I reserve the right to change my mind in the future. Xen is another one of those technologies I've been meaning to get my head around.

5 - |back| -Which is to say, I won't be doing serious work in either language. They're still useful cognitive tools, but the last week or so has me questioning whether they're particularly useful production tools.

6 - |back| -Except that the type signature I gave you is kinda bogus. Generic Name (Map Type Body) wouldn't handle things like partially overlapping types and type variables, so we're really looking at something more like Generic Name (Trie Arg Body).

7 - |back| - Which means that we certainly can't have polymorphic fexprs with this approach. Not that it's a huge loss, mind you, I doubt that's a particularly useful thing given that fexprs essentially always act on things of type s-expression.

Creative Commons License

all articles at langnostic are licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License

Reprint, rehost and distribute freely (even for profit), but attribute the work and allow your readers the same freedoms. Here's a license widget you can use.

The menu background image is Jewel Wash, taken from Dan Zen's flickr stream and released under a CC-BY license