Where communities thrive


  • Join over 1.5M+ people
  • Join over 100K+ communities
  • Free without limits
  • Create your own community
People
Activity
Vesa Karvonen
@polytypic
If you only need to read, then you can e.g. use L.modify to modify the field like in this playground.
Norbert
@norbertpy

Hi folks, trying to understand how L.defaults and L.valueOr works but I fail to do so. Given:

const book = {
  title: 'book of worm',
  pages: [
    { id: 1, content: 'page 1 content' },
    { id: 2, content: 'page 2 content' },
  ]
};

L.modify(
    ['pages', L.elems, L.whereEq({ id: 3 }), L.valueOr({ id: 'unknown' }), 'content'],
    () => 'new content',
    book
);

I expect to see { id: 'unknown', content: 'new content' } to be added to the pages array. Am I wrong?

Vesa Karvonen
@polytypic
In a case like this L.find is the combinator to use. Like this. L.elems only goes through existing elements and L.whereEq also only looks for existing elements (that may be arbitrarily deep within the data structure).
Norbert
@norbertpy
@polytypic Fantastic. Thanks very much.
Vesa Karvonen
@polytypic
No problem. :)
Kyle Mills
@kyle83567_twitter
Hi everyone, really liking partial.lenses, just trying to figure out how to set an object property to false:
/* Lenses a specific modal and returns the default modal if none exists */
export const modalPrismFactory = id => L.compose(id, L.defaults({
    show: false,
}));
/* Lenses a specific modals show property */
export const modalOpenPrismFactory = id => L.compose(modalPrismFactory(id), 'show');
/* Then I want to set */
L.set(modalOptics.modalOpenPrismFactory('1'), false,  { 1: { show: true} }); 
/* The above will return -> {}, I would expect this to return { show: false } */
Vesa Karvonen
@polytypic
L.defaults removes the element in case it is set with the default as in this case. This is by design to support "propagating removal". If you want the default to not be removed then you can use e.g. L.define or L.valueOr. Here is an example using L.valueOr.
Kyle Mills
@kyle83567_twitter
@polytypic - Thanks for the clear example, I'll update my L.defaults logic to valueOr!
Riku Tiira
@rikutiira
Neat, gotta check this at home
Lassi Ketola
@lassiketola_twitter
Hey guys, was browsing through some calmm.js docs and found this
Is there a way to use the response of promise with observables so that when the response gives stuff you push them to store
I want to do the state changes outside my get requests becuase i want to reuse that get request function
Lassi Ketola
@lassiketola_twitter
And is it even good idea to do that?
Josh Bertrand
@abstracthat
U.fromPromise converts a promise to an observable. https://github.com/calmm-js/karet.util/blob/master/README.md#u-frompromise
Lassi Ketola
@lassiketola_twitter
Do you guys have any repositories that i could check out for learning purposes
Vesa Karvonen
@polytypic
My code sandbox profile has mostly Calmm examples, including this one using fetch.
Lassi Ketola
@lassiketola_twitter
Thanks @polytypic
Benjamin Gudehus
@hastebrot
Hi! I try to collect nodes with L.collect. I wonder if there is something like L.choices that executes all branches instead of the first found, so I can also collect intermediate nodes instead of just leaf nodes.
ObservableHQ notebook with data (which contains the nodes) and my attempts with L.collectAs and L.lazy is here: https://observablehq.com/d/79f4018bf8dbc8d3
Benjamin Gudehus
@hastebrot
So, I modified my data structure slightly to allow to navigate to props and childs properties using L.branch().
L.collectAs(
  node => R.pickBy((value, key) => key !== "childs", node),
  L.lazy(forward => 
         L.ifElse(R.whereEq({ type: "node" }), 
                  L.branch({ 
                    props: L.compose(L.optional), 
                    childs: L.compose(L.elems, forward) 
                  }), 
                  L.optional)),
  data
)
Benjamin Gudehus
@hastebrot
I've documented the first two attempts (first is with L.choices, seconds is with L.branch). Still not satisfied with the result. My third attempt is using L.seq, but this does not work with L.collect. Maybe I have to look deeper into how the algebras (map, ap, and of) work used by branchOr1Level().
Benjamin Gudehus
@hastebrot
So, I found out that I can use L.pick to transform the object before I pass it to L.branch. I can refer to the object itself by using [] (or L.identity).
L.get(L.pick({ props: [], childs: "childs" }), { id: 1, childs: 2 })
Vesa Karvonen
@polytypic

Hi @hastebrot !

so I can also collect intermediate nodes instead of just leaf nodes.

This is one of those things that is more subtle and more difficult than it might first appear.

First of all, traversals only target non-overlapping focuses. This might seem like an arbitrary restriction, but it is rather fundamental to the way traversals, which allow both reading and writing, work and what sort of properties they have (laws). So, trying to focus on all nodes, both leaf and internal nodes, in a tree structure cannot be done as such using traverals.

There are several ways around this.

The approach of using L.pick has the problem that the traversal no longer works fully bidirectionally. Convenient as it is L.pick allows one to break the laws of optics. Of course, if you are only reading through the traversal, then L.pick works — as do ordinary functions:

L.get(node => ({node, childs: node.childs}), {id: 1, childs: 2})

If we cannot address overlapping focuses, then a lawful way is to divide the properties of nodes into disjoint subsets such that the children are in one subset and the rest of the properties in another. This way the properties of both internal and leafs nodes can be addressed in a non-overlapping fashion. The L.disjoint combinator has an example of this. Of course, the drawback of this is that the internal nodes properties do not contain the children.

In cases where one wants to modify both both leafs and internal nodes, it is possible to use e.g. L.rewrite to modify the internal nodes as shown in the BST example. This, of course, does not help in the case where one wants to collect the nodes.

A fourth way is to forget bidirectional applicative traversals and use monadic transforms and e.g. L.seq. It is possible to collect the focuses of a monadic transform, but not with the L.collect operation as it works with applicative traversals. The test code has an implementation of collectM that allows collecting the focuses of a monadic transform. One drawback of this approach is that collectM is much more expensive than L.collect.

Benjamin Gudehus
@hastebrot

@polytypic Thank you for the detailed answer. Using L.disjointmakes sense, when I want to transform the tree, i.e. rewrite the paths. I can see the use-case of L.pick still, when I just want to read the nodes.

I also experimented with L.concatAs, L.foldl and L.forEachWith. I want to use them to rewrite the node paths in a tree. Here I need L.tieIx to append the old path and path to rewrite. Most of my problems are solved. Will put the solutions into the notebook when I cleaned/refactored the code.

Josh Bertrand
@abstracthat
@polytypic I think maybe I don't have npm publish rights for karet.xhr. I've merged 1.0.0 release but am getting You do not have permission to publish "karet.xhr". In the npm online dashboard I only see karet under packages. My npm username is joshbertrand. Can you check that on your side real quick?
Vesa Karvonen
@polytypic
@abstracthat How about now?
Josh Bertrand
@abstracthat
Yes! Thank you.
Release is done.
Vesa Karvonen
@polytypic
Seems to work. :tada: Thanks for making the release happen!
Vesa Karvonen
@polytypic

@abstracthat I would be curious to hear about any experiences (good and bad) you've had using the library and how you might compare it to other request libraries you've used. I got the impression you've also used XHRs with L.traverse. I'd love to see examples of any interesting use cases you might have come up with (with or without L.traverse).

I'm asking, because I'm considering having a presentation on the Karet XHR library. Even though the ideas behind it are rather simple, I believe they are still not common knowledge and there are people who might find them interesting and benefit from them.

Riku Tiira
@rikutiira
@polytypic do you mean a live presentation? is it some event I can attend as well?
Vesa Karvonen
@polytypic
A company internal presentation for starters, but it would likely be possible to have a guest there. If I have such a presentation, I will likely take a screen recording.
Josh Bertrand
@abstracthat
Sorry I missed that ping. We were on summer holiday.
I'm very much enjoying using karet.xhr. I think the XHR monad makes a great container for the model layer of our app. It's very flexible to work with.
I've built two abstractions on top of it. Request formats requests (camel to snake case, format url query string, filters, etc) and responses (mostly just snake to camel) from the API. It uses a duplicate request cache so that simultaneous GET requests for the same params share the same XHR.
Then there is Resource which is for setting up our endpoints with some common functionality. This is where I've used L.traverse. Many of our endpoints return related resources as a link (or sometimes an array of id's). So we can declare what related resources we want to load (with an array of stings for the key) and get back the data structure all filled in.
U.thru(initialRequest, XHR.chain(traverseParallelRequests))
The first request uses XHR.chain which takes L.traverse using the XHR.IdentityParallel algebra. This does exactly what I need... I can create an array of XHR's with urls created by the result of the first XHR. Those get sent out as parallel XHR's to the browser. Then I XHR.apply that with a function that knows how to combine the new responses into a single response.
const traverseParallelRequests = L.traverse(
  XHR.IdentityParallel,
  initialResponse =>
    U.thru(
      prepareRelatedResourceRequests(initialResponse, relatedResourcesToLoad),
      XHR.apply(mergeResponsesWithIntitalResponse(initialResponse, relatedResourcesToLoad))
    ),
  traversalLens
)
At the component level it's great. We get to think of it as one request and observe a single XHR.isProgressing and get a single XHR.response. I'm tempted to say the result is magical but I know it's just math.
When you set up an API endpoint with Resource you get convenience methods for interacting with the API as well as a component that makes use of L.collectAs to run the data through a lens that adapts it to a component.
<Patient
  as={ComponentView}
  load={['appointments', 'invoices']}
  view={...adapt to component with lens}
  query={...optional api filters} />
As to challenges I did conceptually have a little bit of a difficult time with the XHR as applicative or monad. But after playing with it I see how they fit together and it's really nice to work with.
One thing I need to do still is allow for nested traversals so that you can load related resources from a related resource and declare that at the top most request. This is somewhat rare in our app, but I'll need to handle it eventually.
Vesa Karvonen
@polytypic

Thanks for the feedback, @abstracthat ! No problem, I assumed you might be on holiday.

Sounds like you are putting Karet XHR to good use!

The way I've been thinking about it is that Karet XHR has two rare features. I say "rare", because I haven't looked at all XHR libraries and I would find it really surprising to not find similar features in other XHR library.

The first rare feature of Karet XHR is simply that the API is comprehensive without using plain callbacks or other forms of side channel communication for anything. For example, Axios uses callbacks for upload and download progress and cancellation tokens. Both are a kind of side-channels where you setup some mutable state to observe or make changes to an ongoing XHR. In Karet XHR the XHR itself and all the properties of a XHR request are exposed via observables obtained from the one observable that represents the request. For progress monitoring you just obtain an observable(s) for the relevant progress information and hook them up where you want them. For cancellation you just make sure that you don't unnecessarily hold on to any observables obtained from the request.

The second rare feature of Karet XHR is that one can compose one or more requests in various ways and still treat the composition as if it were just one request. I think that this kind of composition is only possible because of the first feature of not requiring the use of side channels like callbacks or cancellation tokens. Currently the library provides (parallel) applicative and (sequential) monadic algebras, but there could be others. Axios, as an example again, wraps requests as promises and you can manage multiple promises as usual (e.g. via .then and Promise.all). To get combined progress reporting and cancellation you need to create and pass callbacks (and somehow combine their results) and cancellation token(s) to each individual request. With Karet XHR a composition of requests can be observed just the same as a single request.

Promises are a popular approach for wrapping HTTP requests, but it does seem to me that promises are not always ideal for the purpose. HTTP requests do not just produce a single result and you often want to report progress information (time varying properties) and cancel requests.

Josh Bertrand
@abstracthat
:100:
Josh Bertrand
@abstracthat

@polytypic Curious if you have any ideas on improving this xhrCache. The idea of is to share one request for any identical GET requests that are made before the first has successfully returned. We do this because it is much simpler to let any component that needs data be able to fetch the data itself rather than relying on passing it around through props to different components on the page.

I couldn't sort out returning the cached request and removing it from cache without using a plain object with synchronously computed keys based on the formattedUrl. It works but I can't help but think that there is a more elegant observable approach.

const xhrCache = {}

const makeRequest = method => (endpoint, params = {}, store) => {
  const isGet = R.equals('GET', method)
  const formattedUrl = U.combine([endpoint, params], formatPathWithParams)

  // endpoint/params can be plain or observable so the formattedUrl can be as well.
  // but we need a plain value to use as a key in the xhrCache object
  let currentFormattedUrl
  formattedUrl.onValue && formattedUrl.onValue(url => currentFormattedUrl = url)
  const currentUrl = currentFormattedUrl || formattedUrl

  if (isGet && xhrCache[currentUrl]) return xhrCache[currentUrl]

  const body = U.thru(
    L.get('body', params),
    R.unless(
      R.isNil,
      R.pipe(
        L.getInverse(snakeCamelKeysIso),
        U.stringify
      )
    )
  )

  const request = url =>
    XHR.perform({ body, headers, method, responseType: 'json', timeout: 60 * 1000, url })

  const xhr = U.thru(
    formattedUrl,
    request,
    XHR.map(L.get(snakeCamelKeysIso)),
    XHR.tap(xhr => {
      store && store.set(xhr)
      delete xhrCache[currentUrl]
    })
  )

  if (isGet) xhrCache[currentUrl] = xhr

  return xhr
}