DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Creating a Web Project: Caching for Performance Optimization
  • Scaling DevOps With NGINX Caching: Reducing Latency and Backend Load
  • Develop a Reverse Proxy With Caching in Go
  • Scaling in Practice: Caching and Rate-Limiting With Redis and Next.js

Trending

  • Building a Real-Time Audio Transcription System With OpenAI’s Realtime API
  • Analyzing Techniques to Provision Access via IDAM Models During Emergency and Disaster Response
  • How to Merge HTML Documents in Java
  • Can You Run a MariaDB Cluster on a $150 Kubernetes Lab? I Gave It a Shot
  1. DZone
  2. Data Engineering
  3. Data
  4. State in Functions: Cache, Lazy + Memoization, and Counter

State in Functions: Cache, Lazy + Memoization, and Counter

Dive into functional program as we consider using state in functions. See what use cases might allow for this compromise in functional ideology.

By 
Nawa Manusitthipol user avatar
Nawa Manusitthipol
·
Apr. 26, 18 · Tutorial
Likes (17)
Comment
Save
Tweet
Share
19.6K Views

Join the DZone community and get the full member experience.

Join For Free

Functional programming advocates for programs with no state. Sometimes, however, we need to use state — not for holding data that changes over time like entities — but for other facilitating purposes. There happens to be a way to hold state in Java functions without turning it into an object. This article discusses how to add state to your functions with practical examples: cache, lazy initialization with memoization, and proving indexes.

State for Functions

One of the important principles of Functional Programming (FP) is to avoid having state. State generally makes it more difficult to reason about code, basically, by adding a temporal dimension to the logic. They also represent a side effect — also known as enemy number one of FP. But just like gray, there are fifty shades of functional purity. If having state provides practical benefits AND (big 'AND') they do not cause many problems — such as unpredictability — it is sometimes worth considering it. So, how will we go about having some state in our functions without turning it into an object? By staying a function, the state has no way to unexpectedly leak out, as there is only one way in and out of the function.

Secret Storage Space

There is a secret storage space in Java that most people often overlook ("under-exploited" might be a better word). That storage space is closure constants. Closure constants are the constants that are attached to the lambda that use them. Consider the following code:

public List<Integer> getSmall(int threshold) {
    return list.stream()
        .filter(i -> i <= threshold)
        .collect(toList());
}  


threshold is the closure constant for the lambda i -> i <= threshold. From the look of it, threshold does not seem to be special, as it looks just like any other variables (or parameters) that can be accessed by the code in the scope. But in reality, it is attached to the lambda (created together with the lambda) and tags along with the lambda through the adventure in the filter method. If it is still not clear to you, take a look at another code snippet:

private Predicate<Integer> smallerThan(int threshold) {
    return i -> i <= threshold;
}
...

    List<Integer> smalls
    = list.stream()
        .filter(smallerThan(5))
        .collect(toList());
...  


As you can see, the lambda created inside smallerThan is returned out of the method. And threshold follows along as a good companion, eager to help answer if any given number is too small. This can be seen as a partial application of a function or some form of currying. Now that you know the secret place, let's make use of it.

Quick Cache

Let say you need to read some information from a database or over the network. This information does not get changed often, so it is OK to cache it for a short period of time such as the life of a request. Using a closure constant to hold the value, we can use it to create a cache. The following code demonstrates how to use it to create a cache for any functions:

public static <I, O> Function<I, O> cacheOf(Function<I, O> inFunction) {
    val cache = new ConcurrentHashMap<I, O>();
    return in -> cache.computeIfAbsent(in, inFunction::apply);
}  


NOTE: Lombok's val is used for brevity.

And then you can use the cache function like this ...

@RequestScoped
public class MrScopeBean {
    @Inject
    private CompetitionService competitionService;
    @Inject
    private UserService userService;
    @Inject
    private CityService cityService;

    private final Function<String, String> cityName = cacheOf(cityService::getCityById).andThen(City::getName);
    /** Returns the winner name and the city. */
    public List<String> getWinnerNamesAndCities() {
        return competitionService
            .getWinners().stream()
            .map(userService::getUserById)
            .map(user -> user.getName() + " @ "+ cityName.apply(user.getCityId()))
            .collect(toList());
    }
}  


The function returned from the method cacheOf has a cache embedded inside. So multiple calls for the same city id will only trigger the actual service call once.

WARNING: Improperly implemented caching can cause a memory leak!

Before we go on, let's get this warning out first. Caching improperly can cause a nasty memory leak that results in the certain death of your application. So, take great care when using caches in long-running applications. The cache implementation shown here is reasonable for short-to-medium live caching needs. Since the actual cache map is with the cityName function, once the function is garbage collected, the whole map is freed to be garbage collected as well. Thus, putting the cached-function object (cityName) in the request scoped object or bean will limit its live span there — given it is not leaked to outside this bean.

Lazy Initialization With Memoization

Let's say you have an operation that is expensive to perform, and the result of the operation can be reused within an acceptable period of time. The function below might be useful.

public static <T> Supplier<T> lazy(Supplier<T> supplier) {
  val dummy     = new Object();
  val reference = new AtomicReference<Object>();
  return ()->{
    if (reference.compareAndSet(null, dummy)) {
      try {
        val value = supplier.get();
        reference.set((Supplier<T>)(()->value));
      } catch (RuntimeException e) {
        reference.set(e);
      }
    }
    while (!(reference.get() instanceof Supplier)) {
      if (reference.get() instanceof RuntimeException)
        throw (RuntimeException)reference.get();
    }
    return ((Supplier<T>)reference.get()).get();
  };
}


In this function, a supplier to perform and return something is given as a parameter. An AtomicReference is then used to stored an object as adummy  to mark that the first processing has started. When the computation is done, a supplier of the value is then set to the reference (replacing the dummy). In the case of exception, the exception will then be assigned to the reference (again, replacing the dummy). If other threads get in while the computation is not done yet, that threads will be in the while loop until the supplier is done or an exception is found as the value. It is done this way to ensure that the supplier only run when needed and only run once no matter how many time it is called (even calls from multiple threads). You can see how I test it here. With all that out of the way, the function might be used like this:

@RequestScoped
public class MrScopeBean {
    @Inject
    private ConfigService configService;
    @Inject
    private UserService userService;
    private final Function<String, User> userById = userService::getById;
    private final Supplier<Configurations> configurations = lazy(configService::readConfigFromFile);
    public boolean isQualify(String id) {
        val userPoint = userById.andThen(User::getLoyatyPoint).apply(id);
        val qualPoint = configurations.get().getQualifiedPoint();
        return userPoint >= qualPoint;
    }
}  


In this example code, reading configuration from a file is expensive, so the lazy function is used to make it lazily applied — and its result is memoized. Just like the cache example earlier, the configurations are only stored within the life of if the supplier object configurations.

Counter: Index in a Stream

Let's say you need to have access to the index of elements within the code that uses Java 8's Stream API. For example, given a list, you need to filter it with a certain condition and return the indices of the ones fitting the condition. Here is how you might approach this:

public static <I, O> Function<I, O> withIndex(BiFunction<I, Integer, O> body) {
    val index = new AtomicInteger();
    return input -> body.apply(input, index.getAndIncrement());
}  


The function above will give you access to the index of each element so you can make use of it.

public List<Index> getQualifiedWinersIndexes(List<User> winners) {
  return winners.stream()
    .map(withIndex(Pair::new))  // Pair is a tuple of 2
    .filter(pair->pair._1.getLoyatyPoint() > configurations.get().getQualifiedPoint())
    .map(pair._2)
    .collect(toList());
}  


In the code above, the winners (Users) are streamed and mapped to a pair using withIndex. withIndex take a BiFunction of index and the element (the winner), then returns a pair holding both the index and the winner. Then, we filter the winner that has a qualified loyalty point. Finally, we collect only the index and return it back.

Or if you want to print the element with its index, you might make one with BiConsumer instead.

public static <I, O> Consumer<I> withIndex(BiConsumer<I, Integer> body) {
    val index = new AtomicInteger();
    return input -> body.accept(input, index.getAndIncrement());
}
public void print(List<User> winners) {
    System.out.println(
        winners.stream()
        .filter(winner->winner.getLoyatyPoint() > configurations.get().getQualifiedPoint())
        .map(withIndex((winner, idx)->idx + ": " + winner.getName()))
        .collect(joining("\n"))
    );
}  


The index value is actually the increment of an atomic integer like a counter. So every time the function is called, the index is incremented. Since the function is called once for each iteration, you get an incremental index for each of those. That means the index is not exactly the index of the element in the original list, but the index of the elements that pass through that operation. If the stream passes through filters before going through withIndex, for example, the resulting index will be the index of the elements after the filter, as you see in the second example.

Conclusion

In pure functional programming, functions must not have state. However, there are practical reasons for a function to have state. Closure constants holding mutable objects can be used as private storage of a function. They can be used to store necessary states. Still, proper consideration must be given to ensure benefits without introducing complexity and unpredictability.

What do you think about state in functions? Will you use something like the ideas presented here in your code? Have you used closure constants for something rather than just currying?

Cache (computing) Memoization

Opinions expressed by DZone contributors are their own.

Related

  • Creating a Web Project: Caching for Performance Optimization
  • Scaling DevOps With NGINX Caching: Reducing Latency and Backend Load
  • Develop a Reverse Proxy With Caching in Go
  • Scaling in Practice: Caching and Rate-Limiting With Redis and Next.js

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!