samskivert: Everthing but the kitchen async

07 August 2012

I frequently suffer from Java's complete lack of assistance when it comes to asynchronous programming, but this little multi-phase maneuver really brings home the pain:

protected void findFriends (final Socks.Network sock, Group contents) {
    contents.removeAll();
    final Label status = UI.wrapLabel(_msgs.findingFriends);
    contents.add(status);

    class FriendFinder {
        public void start () {
            // make sure we've authenticated with this network
            sock.authenticate(new Callback<Socks.Member>() {
                public void onSuccess (Socks.Member self) { gotSelf(self); }
                public void onFailure (Throwable err) { fail(_msgs.authErrPre, err); }
            });
        }

        protected void gotSelf (Socks.Member self) {
            // note our own name and id on this network for posterity
            Global.persist.player().socksIds.put(sock.id(), self);

            // get full list of friends for this social network...
            sock.getFriends(new Callback<List<Socks.Member>>() {
                public void onSuccess (List<Socks.Member> friends) { gotFriends(friends); }
                public void onFailure (Throwable err) { fail(_msgs.getFriendsErrPre, err); }
            });
        }

        protected void gotFriends (List<Socks.Member> friends) {
            Json.Writer writer = json().newWriter();
            writer.array();
            for (Socks.Member friend : friends) writer.value(friend.userId);
            writer.end();

            final Map<String,Socks.Member> fmap = Maps.newHashMap();
            for (Socks.Member friend : friends) fmap.put(friend.userId, friend);

            // filter our full list of friends, to obtain those that play d11s
            net().post(Global.serviceURL("filter"), writer.write(), new Callback<String>() {
                public void onSuccess (String data) { gotFiltered(fmap, data); }
                public void onFailure (Throwable err) { fail(_msgs.filterErrPre, err); }
            });
        }

        protected void gotFiltered (Map<String,Socks.Member> fmap, String data) {
            try {
                List<Socks.Member> filtered = Lists.newArrayList();
                Json.Array fids = json().parseArray(data);
                for (int ii = 0, ll = fids.length(); ii < ll; ii++) {
                    Socks.Member friend = fmap.get(fids.getString(ii));
                    if (friend != null) filtered.add(friend);
                    else log.warning("Got unknown friend from server",
                                     "id", fids.getString(ii), "fmap", fmap);
                }

                // store the filtered friends locally
                Global.persist.socks().storeFriends(sock.id(), filtered);

                // and finally update the user interface and report success
                int added = addFriends(sock.id(), filtered);
                setStatus(_msgs.foundFriends(added, SocksUI.name(sock)));

            } catch (Exception e) {
                log.warning("Failed to parse filtered friends", "data", data, e);
                fail(_msgs.filterParseErrPre, e);
            }
        }

        protected void fail (String prefix, Throwable cause) {
            setStatus(prefix + ": " + cause.getMessage());
        }

        protected void setStatus (String message) {
            status.text.update(message);
        }
    }
    new FriendFinder().start();
}

I've already taken great liberties with "Java style" to achieve a level of concision that allows at least some of the control flow to remain visible, but it still burns the eyes.

Just for fun, let's imagine what that might look like if Java supported something like C#'s async mechanism (wherein the resulting code is the same as the above, but the compiler takes care of the boilerplate behind the scenes):

protected void findFriends (final Socks.Network sock, Group contents) {
    contents.removeAll();
    final Label status = UI.wrapLabel(_msgs.findingFriends);
    contents.add(status);

    String errpre = _msgs.authErrPre;
    try {
        Socks.Member self = await sock.authenticate();

        // note our own name and id on this network for posterity
        Global.persist.player().socksIds.put(sock.id(), self);

        // get full list of friends for this social network...
        errpre = _msgs.getFriendsErrPre;
        List<Socks.Member> friends = await sock.getFriends();
        Map<String,Socks.Member> fmap = Maps.newHashMap();
        for (Socks.Member friend : friends) fmap.put(friend.userId, friend);

        // filter our full list of friends, to obtain those that play d11s
        Json.Writer writer = json().newWriter();
        writer.array();
        for (Socks.Member friend : friends) writer.value(friend.userId);
        writer.end();
        errpre = _msgs.filterErrPre;
        String data = await net().post(Global.serviceURL("filter"), writer.write());
        Json.Array fids = json().parseArray(data);
        List<Socks.Member> filtered = Lists.newArrayList();
        for (int ii = 0, ll = fids.length(); ii < ll; ii++) {
            Socks.Member friend = fmap.get(fids.getString(ii));
            if (friend != null) filtered.add(friend);
            else log.warning("Got unknown friend from server",
                             "id", fids.getString(ii), "fmap", fmap);
        }

        // store the filtered friends locally
        Global.persist.socks().storeFriends(sock.id(), filtered);

        // and finally update the user interface and report success
        int added = addFriends(sock.id(), filtered);
        status.text.update(_msgs.foundFriends(added, SocksUI.name(sock)));

    } catch (Exception e) {
        status.text.update(errpre + ": " + e.getMessage());
    }
}

While we're dreaming, let's rewrite it all in Scala (assuming the use of the delimited continuations plugin to do the appropriate rewriting):

def findFriends (sock :Socks.Network, contents :Group) {
  contents.removeAll()
  val status = UI.wrapLabel(_msgs.findingFriends)
  contents.add(status)

  var errpre = _msgs.authErrPre
  try {
    val self = await sock.authenticate

    // note our own name and id on this network for posterity
    Global.persist.player.socksIds.put(sock.id, self)

    // get full list of friends for this social network...
    errpre = _msgs.getFriendsErrPre
    val friends = await sock.getFriends

    // filter our full list of friends, to obtain those that play d11s
    errpre = _msgs.filterErrPre
    val json = json.newWriter.array(friends map(_.userId)).write
    val farray = json.parseArray(await net.post(Global.serviceURL("filter"), json))
    val fids = (0 to farray.length) map(farray.getString) toSet
    val fmap = friends map(f => f.userId -> f) toMap
    val filtered = fmap filterKeys(fids) values
    if (fids.size != filtered.size) {
      log.warnning("Got unknown friends(s) from server", "friends", friends,
                   "ids", (fids -- filtered.map(_.userId)))
    }

    // store the filtered friends locally
    Global.persist.socks.storeFriends(sock.id, filtered)

    // and finally update the user interface and report success
    val added = addFriends(sock.id, filtered)
    status.text.update(_msgs.foundFriends(added, SocksUI.name(sock)))

  } catch {
    case e => status.text.update(errpre + ": " + e.getMessage)
  }
}

Alas, I can't use Scala on my current project. I'll shut up now and get back to the salt mines.

©1999–2015 Michael Bayne