My Journey into Functional Programming with Kotlin and Svelte Kit

Functional programming in Typescript with fp-ts and Svelte Kit.

Embarking on my journey into functional programming, I initially delved into Kotlin. Concurrently, I was learning Svelte Kit by building a simple Notes app. Although the allure of using both languages was enticing, fate led me to discover the fp-ts library for Node.js. This prompted me to rewrite some API endpoints, incorporating intriguing functional concepts, especially in error handling. This blog post serves as a comparative exploration of the imperative and functional styles, aiming to foster an appreciation for the evolutionary shift in approach.

Imperative style

Let’s start by dissecting a straightforward Svelte Typescript API endpoint that employs an imperative style for a GET request in a Notes app. For simplicity, the function assumes that user authentication has made the user ID available on the locals object.

The imperative style handles errors at the API level with different status codes for various scenarios: Note not found (404), user not found (403), unauthorized access (403), and unexpected errors (500).

Now imagine the other API methods like PATCH and DELETE. What would they look like and how much repetition would be required such as checking item existence and returning not found. One option would be creating shared services that return results that would then be mapped to an API result. At this point, something was telling me that this could be solved in better style using a function approach.

The functional style

Now, let’s delve into the functional approach:

The TE is a naming convention in fp-ts used for TaskEither which is basically akin of the Either type for asynchronous operations. In the world of Node, async operations have become the norm fp-ts provices TaskEither to handle them.

Using the pipe method, we can start to chain operations. Let’s start from the top:

  • TE.Do initialises a sequence of operations.
  • TE.bind('user', () => getUser({ id: locals.user.id })) retrieves the user with the given ID. The ! operator asserts that locals.user.id is not null or undefined. The bind method adds the user property to the TaskEither right container and is automatically available in the next method in the chain.
  • TE.bind('note', () => getNoteById({ id: params.id})) retrieves the note with the given ID. Again, the ! operator asserts that params.id is not null or undefined.
  • TE.flatMap(({ user, note }) => isNoteOwner({ user, note }))checks if the retrieved user is the owner of the retrieved note. These two objects are made available because of the bind method.
  • TE.mapLeft(mapToApiError) maps any errors that occur during these operations to API errors.
  • Finally, the TE.match method is used to handle the result of the operations. The first function passed to it handles the error case, while the second function handles the success case.

Separation of concerns

Digging deeper, the methods for data retrieval and validation return a Server Error in the left container. For example, the getUser method has the following signature:

The method is part of a repository layer and should not have any knowledge about API statuses like 404 or 500 but it should be able to return specific errors like database connection error, or a record not found error. In typescript we could take advantage of union types to handle this:

We can proceed to compose methods that operate on the business errors (ServerError) and when we’re ready it can then be mapped to an API error where it matters.

Conclusion

Although I’m only scratching the surface of functional programming, the moment I discovered TaskEither in fp-ts I knew it was enough to build something practical.

In my perspective, the functional approach yields cleaner, more predictable, and idiomatic code at the API level. It provides a systematic and structured way to handle errors, promoting separation of concerns and ensuring a more robust and maintainable codebase. Embracing functional programming in this context proves to be a transformative journey, enhancing the overall development experience.