Skip to content

Instantly share code, notes, and snippets.

@gcanti
Last active June 1, 2020 03:43
Show Gist options
  • Save gcanti/66d223b8e03baa7be12611023afa1717 to your computer and use it in GitHub Desktop.
Save gcanti/66d223b8e03baa7be12611023afa1717 to your computer and use it in GitHub Desktop.
MTL-style using fp-ts
import { Monad, Monad1 } from 'fp-ts/lib/Monad'
import { HKT, URIS, Type } from 'fp-ts/lib/HKT'
import { liftA2 } from 'fp-ts/lib/Apply'
import { flatten } from 'fp-ts/lib/Chain'
import { Newtype, iso } from 'newtype-ts'
// Adapted from https://tech.iheart.com/why-fp-its-the-composition-f585d17b01d3
//
// newtypes
//
type Url = Newtype<'Url', string>
type SessionToken = Newtype<'SessionToken', string>
type UserId = Newtype<'UserId', string>
type FBToken = Newtype<'FBToken', string>
type FBPost = Newtype<'FBPost', string>
//
// MTL style
//
interface MonadUser<M> extends Monad<M> {
validateUser: (token: SessionToken) => HKT<M, UserId>
facebookToken: (uid: UserId) => HKT<M, FBToken>
}
interface MonadUser1<M extends URIS> extends Monad1<M> {
validateUser: (token: SessionToken) => Type<M, UserId>
facebookToken: (uid: UserId) => Type<M, FBToken>
}
interface MonadFB<M> extends Monad<M> {
findPost: (url: Url) => HKT<M, FBPost>
sendLike: (fbToken: FBToken) => (post: FBPost) => HKT<M, boolean>
}
interface MonadFB1<M extends URIS> extends Monad1<M> {
findPost: (url: Url) => Type<M, FBPost>
sendLike: (fbToken: FBToken) => (post: FBPost) => Type<M, boolean>
}
//
// program
//
function likePost<M extends URIS>(
M: MonadUser1<M> & MonadFB1<M>
): (token: SessionToken) => (url: Url) => Type<M, boolean>
function likePost<M>(M: MonadUser<M> & MonadFB<M>): (token: SessionToken) => (url: Url) => HKT<M, boolean> {
return token => url => {
const mToken = M.chain(M.validateUser(token), uid => M.facebookToken(uid))
const mPost = M.findPost(url)
const mmResult = liftA2(M)(M.sendLike)(mToken)(mPost)
return flatten(M)(mmResult)
}
}
//
// Isos
//
const isoUrl = iso<Url>()
const isoSessionToken = iso<SessionToken>()
const isoUserId = iso<UserId>()
const isoFBToken = iso<FBToken>()
const isoFBPost = iso<FBPost>()
//
// IO instance
//
import { URI as IOURI, io, IO } from 'fp-ts/lib/IO'
const ioInstance: MonadUser1<IOURI> & MonadFB1<IOURI> = {
...io,
validateUser: token => io.of(isoUserId.wrap(`UserId(${token})`)),
facebookToken: uid => io.of(isoFBToken.wrap(`FBToken(${uid})`)),
findPost: url => io.of(isoFBPost.wrap(`FBPost(${url})`)),
sendLike: token => post => {
return new IO(() => {
console.log(`sending a like, with fbToken "${token}" and post "${post}"`)
return true
})
}
}
const sessionToken = isoSessionToken.wrap('SessionToken(foo)')
const url = isoUrl.wrap('https://whatever')
console.log(likePost(ioInstance)(sessionToken)(url).run())
// sending a like, with fbToken "FBToken(UserId(SessionToken(foo)))" and post "FBPost(https://whatever)"
// true
//
// Task instance
//
import { URI as TaskURI, task, Task } from 'fp-ts/lib/Task'
// helper
const now = Date.now()
const delay = <A>(a: A) => (n: number): Task<A> =>
new Task(
() =>
new Promise(resolve => {
setTimeout(() => {
console.log(`${a} after ${(Date.now() - now) / 1000}`)
resolve(a)
}, n)
})
)
const taskInstance: MonadUser1<TaskURI> & MonadFB1<TaskURI> = {
...task,
validateUser: token => delay(isoUserId.wrap(`UserId(${token})`))(1000),
facebookToken: uid => delay(isoFBToken.wrap(`FBToken(${uid})`))(500),
findPost: url => delay(isoFBPost.wrap(`FBPost(${url})`))(2000),
sendLike: token => post => {
console.log(`sending a like, with fbToken "${token}" and post "${post}"`)
return delay(true)(1000)
}
}
likePost(taskInstance)(sessionToken)(url)
.run()
.then(result => console.log(result))
// UserId(SessionToken(foo)) after 1.003
// FBToken(UserId(SessionToken(foo))) after 1.505
// FBPost(https://whatever) after 2.002
// sending a like, with fbToken "FBToken(UserId(SessionToken(foo)))" and post "FBPost(https://whatever)"
// true after 3.004
// true
@wclr
Copy link

wclr commented Oct 21, 2017

For new types I usually do just:

type AuthToken = string & {__authToken: true}

const authorize = (token: AuthToken) => ...

authorize('secret' as AuthToken)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment