Haskell, 62 6161 59 bytes
e=[]:e z=zipWith a!b=[unwords.zipWithz(++)r<$>foldr(zipWithz(:))e b|r<-a] Example usage:
Prelude> [["a","b"],["c","e"]] ! [["f","g"],["h","i"]] [["af bh","ag bi"],["cf eh","cg ei"]] I found a way to get a transpose function in one byte shorter than using the import:.
import Data.List;transpose e=[]:e;foldr(zipWith(:))e As the code then uses zipWith twice, two more bytes can be saved with the abbreviation z=zipWith.
Old version with import: (62 bytes)
import Data.List a!b=[unwords.zipWith(++)r<$>transpose b|r<-a] This is quite similar to my answer to non-symbolic matrix multiplication: a!b=[sum.zipWith(*)r<$>transpose b|r<-a], substituting the multiplication (*) with string concatenation (++) and sum with unwords which concatenates a list of strings with a space in between. The import is needed for the transpose function, so all in all the transposition of the second matrix uses up half of the bytes ...
Old version without import: (64 bytes)
a![]=[];a!b=(unwords.zipWith(++)[h|h:_<-b]<$>a):a![s:t|_:s:t<-b] With the import and transpose function taking up so much bytes, I tried solving the task without import. So far this approach turned out being two bytes longer, but it might be more golfable. Edit: The other approach at the top now beats the import!
The list comprehension [s:t|_:s:t<-b] gets the non-empty tails of the lists in b, using just [t|_:t<-b] to get the tails would be 4 bytes shorter (even beating the import version) but append an empty row like ["","",""] to the matrix which I suppose is not allowed.