TicTacToe game in Haskell
Some thoughts about your Tic-Tac-Toe version.

You made a confusion between Tile and Player. You assume a Player is the same as a Tile but it is not. A Tile can be Empty but a Player cannot be empty. You should separate these two notions. It will make your declarations clearer on what you expect. What if I call "gameLoop Empty startBoard" ?

The putStr function does not flush immediately its output in a compiled program. Your program asks the user for a row and a column and displays these labels after the user inputs.

Is it natural to ask for the row before the column ?

You have intertwined the game logic with the user interface in the gameLoop function. What will you do if you want to implement a computer player ? Or have a GUI to play the game ? You will have to patch your gameLoop function though the game logic has not changed.

Your solution is locked to the fact that your board is a 3×3 grid with a 3 tiles win. For example, your inRange functions works for row indices AND column indices, which are not the same. You use the constants 2 and 3 in your code (inRange, startBoard and whoWon), but nothing implies a relation between them. The 3 rows and 3 columns are characteristics of the Board, not of your functions.

Messages could be more specific. When you tell the user "Invalid position", there’s no way to know if it’s because the coordinates are out of bounds or because the tile has already been played.

Note: there are many Tic-Tac-Toe game related questions on CodeReview, you should take a look at:


Tic Tac Toe game in Haskell (a brute first attempt)
Tic Tac Toe game in haskell follow up (its follow up)
TicTacToe in Haskell (a version using Lens)
m,n,k-game (i.e. generalized tic-tac-toe) (its generalization)