Symbolic matrix multiplication
Haskell, 62 61 bytes

e=[]:e
a!b=[unwords.zipWith(++)r&lt;$&gt;foldr(zipWith(:))e b|r&lt;-a]

Try it online! Example usage:
Prelude&gt; [[&quot;a&quot;,&quot;b&quot;],[&quot;c&quot;,&quot;e&quot;]] ! [[&quot;f&quot;,&quot;g&quot;],[&quot;h&quot;,&quot;i&quot;]]
[[&quot;af bh&quot;,&quot;ag bi&quot;],[&quot;cf eh&quot;,&quot;cg ei&quot;]]

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


Old version with import: (62 bytes)
import Data.List
a!b=[unwords.zipWith(++)r&lt;$&gt;transpose b|r&lt;-a]

Try it online!
This is quite similar to my answer to non-symbolic matrix multiplication: a!b=[sum.zipWith(*)r&lt;$&gt;transpose b|r&lt;-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:_&lt;-b]&lt;$&gt;a):a![s:t|_:s:t&lt;-b]

Try it online!
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&lt;-b] gets the non-empty tails of the lists in b, using just [t|_:t&lt;-b] to get the tails would be 4 bytes shorter (even beating the import version) but append an empty row like [&quot;&quot;,&quot;&quot;,&quot;&quot;] to the matrix which I suppose is not allowed.