First of all, you need to know that Shake is a library written in Haskell, and it's meant to be a better replacement for make. Let's start by installing cabal and then shake:
apt-get install cabal-install cabal update cabal install shake
TL;DR; this is the final Build.hs file:
#!/usr/bin/env runhaskell import Development.Shake import Development.Shake.FilePath import Development.Shake.Sys import Control.Applicative hiding ((*>)) app = "bestValaApp" sources = words "file1.vala file2.vala file3.vala" packages = words "gtk+-3.0 glib-2.0 gobject-2.0" cc = "cc" valac = "valac" pkgconfig = "pkg-config" -- derived csources = map (flip replaceExtension ".c") sources cobjects = map (flip replaceExtension ".o") csources main = shakeArgs shakeOptions $ do want [app] app *> \out -> do need cobjects pkgconfigflags <- pkgConfig $ ["--libs"] ++ packages sys cc "-fPIC -o" [out] pkgconfigflags cobjects cobjects **> \out -> do let cfile = replaceExtension out ".c" need [cfile] pkgconfigflags <- pkgConfig $ ["--cflags"] ++ packages sys cc "-ggdb -fPIC -c -o" [out, cfile] pkgconfigflags csources *>> \_ -> do let valapkgflags = prependEach "--pkg" packages need sources sys valac "-C -g" valapkgflags sources -- utilities prependEach x = foldr (\y a -> x:y:a)  pkgConfig args = (words . fst) <$> (systemOutput pkgconfig args)
Just tweak app, sources and packages to match your needs, chmod +x Build.hs then run ./Build.hs .
The words function splits a string by spaces to get a list of strings, e.g. ["file1.vala", "file2.vala", "file3.vala"].
The csources variable maps .vala file names to .c file names. Same goes for cobjects. It's the equivalent of $(subst .vala,.c,$(SOURCES)) you'd do with make.
There it comes the main. The shakeArgs shakeOptions part will run shake with default options. Shake provides handy command line options similar to make, run ./Build.hs -h for help.
The want [app] tells shake we want to build the app object by default. That's equivalent to the usual first make rule all: $(APP).
Then we define how to build the executable app with app *> \out -> do. We tell shake the dependencies with need cobjects. This is similar to $(APP): $(COBJECTS) in make but not equivalent. In shake dependencies are not static like in many other build systems. This is one of the most interesting shake features.
The rest is quite straightforward to understand.
Then we define how to build each .o object with cobjects **> \out -> do. Here the out variable contains the actual .o required to be built, equivalent to $@ in make. Then we need [cfile], in order to simulate %.o: %.c like in make.
One more feature shake has out-of-the-box that make doesn't is how to generate more files from a single command. With make you'd use a .stamp file due to valac generating several .c files out of .vala files. Then use the .stamp as dependency.
With shake instead we consistently define how to build .c files with csources *>> \_ -> do, then shake will do the rest.
The shake project is very active. You can read this tutorial to learn Haskell basics, and the reference docs of shake. The author homepage has links to cool presentations of the shake build system.