content: use framework "Foundation" -- Yosemite onwards, for record-handling functions
  
  -- HASH JOIN -----------------------------------------------------------------
  
  -- hashJoin :: [Record] -> [Record] -> String -> [Record]
  on hashJoin(tblA, tblB, strJoin)
      set {jA, jB} to splitOn("=", strJoin)
  
      script instanceOfjB
          on |λ|(a, x)
              set strID to keyValue(x, jB)
  
              set maybeInstances to keyValue(a, strID)
              if maybeInstances is not missing value then
                  updatedRecord(a, strID, maybeInstances & {x})
              else
                  updatedRecord(a, strID, [x])
              end if
          end |λ|
      end script
  
      set M to foldl(instanceOfjB, {name:"multiMap"}, tblB)
  
      script joins
          on |λ|(a, x)
              set matches to keyValue(M, keyValue(x, jA))
              if matches is not missing value then
                  script concat
                      on |λ|(row)
                          x & row
                      end |λ|
                  end script
  
                  a & map(concat, matches)
              else
                  a
              end if
          end |λ|
      end script
  
      foldl(joins, {}, tblA)
  end hashJoin
  
  -- TEST ----------------------------------------------------------------------
  on run
      set lstA to [¬
          {age:27, |name|:"Jonah"}, ¬
          {age:18, |name|:"Alan"}, ¬
          {age:28, |name|:"Glory"}, ¬
          {age:18, |name|:"Popeye"}, ¬
          {age:28, |name|:"Alan"}]
  
      set lstB to [¬
          {|character|:"Jonah", nemesis:"Whales"}, ¬
          {|character|:"Jonah", nemesis:"Spiders"}, ¬
          {|character|:"Alan", nemesis:"Ghosts"}, ¬
          {|character|:"Alan", nemesis:"Zombies"}, ¬
          {|character|:"Glory", nemesis:"Buffy"}, ¬
          {|character|:"Bob", nemesis:"foo"}]
  
      hashJoin(lstA, lstB, "name=character")
  end run
  
  
  -- RECORD FUNCTIONS ----------------------------------------------------------
  
  -- keyValue :: String -> Record -> Maybe a
  on keyValue(rec, strKey)
      set ca to current application
      set v to (ca's NSDictionary's dictionaryWithDictionary:rec)'s ¬
          objectForKey:strKey
      if v is not missing value then
          item 1 of ((ca's NSArray's arrayWithObject:v) as list)
      else
          missing value
      end if
  end keyValue
  
  -- updatedRecord :: Record -> String -> a -> Record
  on updatedRecord(rec, strKey, varValue)
      set ca to current application
      set nsDct to (ca's NSMutableDictionary's dictionaryWithDictionary:rec)
      nsDct's setValue:varValue forKey:strKey
      item 1 of ((ca's NSArray's arrayWithObject:nsDct) as list)
  end updatedRecord
  
  
  -- GENERIC FUNCTIONS ---------------------------------------------------------
  
  -- foldl :: (a -> b -> a) -> a -> [b] -> a
  on foldl(f, startValue, xs)
      tell mReturn(f)
          set v to startValue
          set lng to length of xs
          repeat with i from 1 to lng
              set v to |λ|(v, item i of xs, i, xs)
          end repeat
          return v
      end tell
  end foldl
  
  -- map :: (a -> b) -> [a] -> [b]
  on map(f, xs)
      tell mReturn(f)
          set lng to length of xs
          set lst to {}
          repeat with i from 1 to lng
              set end of lst to |λ|(item i of xs, i, xs)
          end repeat
          return lst
      end tell
  end map
  
  -- Lift 2nd class handler function into 1st class script wrapper
  -- mReturn :: Handler -> Script
  on mReturn(f)
      if class of f is script then
          f
      else
          script
              property |λ| : f
          end script
      end if
  end mReturn
  
  -- splitOn :: Text -> Text -> [Text]
  on splitOn(strDelim, strMain)
      set {dlm, my text item delimiters} to {my text item delimiters, strDelim}
      set lstParts to text items of strMain
      set my text item delimiters to dlm
      return lstParts
  end splitOn
  